diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index ab5edc5..e8bfba1 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -29,6 +29,7 @@ INSTALLED_APPS = [ 'corsheaders', 'django_celery_results', 'celery', + 'gvsdsdk', 'users', 'orders', 'products', diff --git a/backend/models.py b/backend/models.py index 87f2b39..0c71dae 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1,8 +1,9 @@ from django.db import models +from gvsdsdk.model_base import QModel # ==================== 1. 角色表 ==================== -class Role(models.Model): +class Role(QModel): """角色表""" role_id = models.AutoField(primary_key=True, verbose_name='角色ID') role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码') @@ -25,7 +26,7 @@ class Role(models.Model): # ==================== 2. 权限表 ==================== -class Permission(models.Model): +class Permission(QModel): """权限表""" perm_id = models.AutoField(primary_key=True, verbose_name='权限ID') perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码') @@ -47,7 +48,7 @@ class Permission(models.Model): # ==================== 3. 角色-权限关联表 ==================== -class RolePermission(models.Model): +class RolePermission(QModel): """角色权限关联表""" id = models.AutoField(primary_key=True) role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') @@ -65,7 +66,7 @@ class RolePermission(models.Model): # ==================== 4. 用户-角色关联表 ==================== -class UserRole(models.Model): +class UserRole(QModel): """用户角色关联表""" id = models.AutoField(primary_key=True) account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段 @@ -83,7 +84,7 @@ class UserRole(models.Model): return f"{self.account_id} -> {self.role.role_name}" -class AbnormalUserLog(models.Model): +class AbnormalUserLog(QModel): """记录恶意尝试越权操作的用户信息""" # 账号信息 account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)') @@ -126,7 +127,7 @@ class AbnormalUserLog(models.Model): -class ShangjiaRiTongji(models.Model): +class ShangjiaRiTongji(QModel): yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID') riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 @@ -156,7 +157,7 @@ class ShangjiaRiTongji(models.Model): -class DashouRiTongji(models.Model): +class DashouRiTongji(QModel): yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID') riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 @@ -185,7 +186,7 @@ class DashouRiTongji(models.Model): -class ZuzhangRiTongji(models.Model): +class ZuzhangRiTongji(QModel): yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID') riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 @@ -207,7 +208,7 @@ class ZuzhangRiTongji(models.Model): -class GuanshiRiTongji(models.Model): +class GuanshiRiTongji(QModel): yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 @@ -228,7 +229,7 @@ class GuanshiRiTongji(models.Model): -class GuanshiXufeiRiTongji(models.Model): +class GuanshiXufeiRiTongji(QModel): yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 @@ -248,7 +249,7 @@ class GuanshiXufeiRiTongji(models.Model): # models.py 中添加 -class TixianRiTongji(models.Model): +class TixianRiTongji(QModel): """ 每日提现统计表(按身份统计当天提现总额) """ diff --git a/backend/urls.py b/backend/urls.py index 4744987..f1a191c 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -11,6 +11,7 @@ from .view import (GetRolePermissionView, ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView, ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView, ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView, + FineApplyView, PunishDashouView, FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView) @@ -79,6 +80,8 @@ urlpatterns = [ path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表 path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款 path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款 + path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请 + path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手 path('hqbkxx', HqbkxxView.as_view(), name='获取板块数据'), # 获取板块数据 path('bkxg', BkxgView.as_view(), name='板块增删改查'), # 板块增删改 diff --git a/backend/utils.py b/backend/utils.py index e99de71..6c0293d 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -13,6 +13,8 @@ from django.db.models import F from datetime import date from decimal import Decimal +from gvsdsdk.fluent import db, func, FQ + logger = logging.getLogger(__name__) @@ -39,7 +41,7 @@ def _record_abnormal(account_id, request, attempt_type, detail=''): request_params = json.dumps(request.query_params.dict(), ensure_ascii=False) if request.query_params else '' request_body = request.body.decode('utf-8', errors='ignore')[:5000] - AbnormalUserLog.objects.create( + AbnormalUserLog.query.create( account_id=account_id, ip_address=real_ip, real_ip=real_ip, @@ -96,9 +98,9 @@ def verify_kefu_permission(request, username_from_frontend=None): # ---------- 4. 获取用户的所有权限(通过角色) ---------- # 步骤1:获取用户的所有角色ID - role_ids = UserRole.objects.filter(account_id=current_user.phone).values_list('role_id', flat=True) + role_ids = UserRole.query.filter(account_id=current_user.phone).values_list('role_id', flat=True) # 步骤2:通过角色ID获取权限编码 - permissions = list(Permission.objects.filter( + permissions = list(Permission.query.filter( rolepermission__role_id__in=role_ids ).values_list('perm_code', flat=True).distinct()) @@ -156,7 +158,7 @@ def update_dashou_daily_by_action(yonghuid, amount, action): return # 无效行为 if update_fields: - DashouRiTongji.objects.filter(id=stat.id).update(**update_fields) + DashouRiTongji.query.filter(id=stat.id).update(**update_fields) def update_shangjia_daily(yonghuid, amount, action): @@ -212,7 +214,7 @@ def update_shangjia_daily(yonghuid, amount, action): return # 执行原子更新 - ShangjiaRiTongji.objects.filter(id=stat.id).update(**update_fields) + ShangjiaRiTongji.query.filter(id=stat.id).update(**update_fields) @@ -242,7 +244,7 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): stat.yaoqing_dashou_shu = 1 stat.save() else: - GuanshiRiTongji.objects.filter(id=stat.id).update( + GuanshiRiTongji.query.filter(id=stat.id).update( yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1 ) @@ -255,7 +257,7 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): stat.shouru_zonge = amount stat.save() else: - GuanshiRiTongji.objects.filter(id=stat.id).update( + GuanshiRiTongji.query.filter(id=stat.id).update( chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1, shouru_zonge=F('shouru_zonge') + amount ) @@ -268,7 +270,7 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): stat.shouru_zonge = amount stat.save() else: - GuanshiRiTongji.objects.filter(id=stat.id).update( + GuanshiRiTongji.query.filter(id=stat.id).update( shouru_zonge=F('shouru_zonge') + amount ) @@ -328,7 +330,7 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): if action == 2: update_fields['shouru_zonge'] = F('shouru_zonge') + amount - GuanshiRiTongji.objects.filter(id=stat.id).update(**update_fields)''' + GuanshiRiTongji.query.filter(id=stat.id).update(**update_fields)''' def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): """ @@ -348,7 +350,7 @@ def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): } ) if not created: - GuanshiXufeiRiTongji.objects.filter(id=stat.id).update( + GuanshiXufeiRiTongji.query.filter(id=stat.id).update( xufei_zongshu=F('xufei_zongshu') + 1, xufei_shouyi=F('xufei_shouyi') + xufei_jine ) @@ -392,13 +394,13 @@ def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')): else: # 已存在记录,使用 F 表达式原子累加 if action == 1: - ZuzhangRiTongji.objects.filter(id=stat.id).update( + ZuzhangRiTongji.query.filter(id=stat.id).update( yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 ) elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') if amount > 0: - ZuzhangRiTongji.objects.filter(id=stat.id).update( + ZuzhangRiTongji.query.filter(id=stat.id).update( shouru_zonge=F('shouru_zonge') + amount, fenyong_jine=F('fenyong_jine') + amount ) @@ -419,7 +421,7 @@ def update_tixian_daily_stat(leixing, amount): defaults={'total_amount': amount} ) if not created: - TixianRiTongji.objects.filter(id=stat.id).update( + TixianRiTongji.query.filter(id=stat.id).update( total_amount=F('total_amount') + amount ) @@ -461,7 +463,7 @@ def _get_fadan_permission_config(): perm_map = dict(_DEFAULT_FADAN_PERM_TO_SHENFEN) generic_codes = list(_FADAN_GENERIC_PERMS) try: - rows = Permission.objects.filter( + rows = Permission.query.filter( Q(perm_name__icontains='罚款') | Q(perm_name__icontains='罚单') ).values('perm_code', 'perm_name') for row in rows: diff --git a/backend/view.py b/backend/view.py index 87cc5f9..8e41b10 100644 --- a/backend/view.py +++ b/backend/view.py @@ -19,7 +19,8 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import Paginator from django.db import models, transaction, IntegrityError -from django.db.models import Q, Count, Sum, F, Exists, OuterRef +from django.db.models import Q, Count, Sum, F, Exists, OuterRef +from gvsdsdk.fluent import db, func, FQ from django.db.models.functions import TruncDate, TruncMonth, TruncYear from django.contrib.auth.hashers import make_password from django.views.decorators.csrf import csrf_exempt @@ -95,6 +96,34 @@ from .serializers import PopupPageSerializer logger = logging.getLogger('houtai') SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd'] +# 罚单权限 → 被处罚人身份 映射 +PERMISSION_TO_SHENFEN = { + '100001': 1, # 管理打手权限 → 可处罚打手(shenfen=1) + '100002': 2, # 管理管事权限 → 可处罚管事(shenfen=2) + '100003': 3, # 管理商家权限 → 可处罚商家(shenfen=3) + '100004': 4, # 管理组长权限 → 可处罚组长(shenfen=4) +} + +# 被处罚人身份 → 用户扩展表属性名 映射 +SHENFEN_PROFILE_MAP = { + 1: 'dashou_profile', # 打手 + 2: 'guanshi_profile', # 管事 + 3: 'shangjia_profile', # 商家 + 4: 'zuzhang_profile', # 组长 +} + + +def check_fadan_permission(permissions, shenfen): + """ + 检查当前权限是否允许操作指定身份的罚单。 + permissions: 用户拥有的权限代码列表 + shenfen: 被处罚人身份(1=打手, 2=管事, 3=商家, 4=组长) + """ + for perm_code, target_shenfen in PERMISSION_TO_SHENFEN.items(): + if perm_code in permissions and target_shenfen == shenfen: + return True + return False + class GetRolePermissionView(APIView): @@ -119,17 +148,17 @@ class GetRolePermissionView(APIView): return Response({'code': 403, 'msg': '您无权访问此页面'}) # 3. 获取所有权限 - all_perms = Permission.objects.all().values('perm_code', 'perm_name', 'description') + all_perms = Permission.query.all().values('perm_code', 'perm_name', 'description') permissions_list = list(all_perms) # 4. 获取所有角色,并统计每个角色下的用户数和关联的权限 - roles = Role.objects.all() + roles = Role.query.all() roles_data = [] for role in roles: # 统计该角色下的用户数 - user_count = UserRole.objects.filter(role=role).count() + user_count = UserRole.query.filter(role=role).count() # 获取该角色绑定的权限 - role_perms = RolePermission.objects.filter(role=role).select_related('permission') + role_perms = RolePermission.query.filter(role=role).select_related('permission') perms_of_role = [ { 'perm_code': rp.permission.perm_code, @@ -181,7 +210,7 @@ class ModifyRolePermissionView(APIView): return Response({'code': 400, 'msg': '缺少角色编码'}) try: - role = Role.objects.get(role_code=role_code) + role = Role.query.get(role_code=role_code) except Role.DoesNotExist: return Response({'code': 404, 'msg': '角色不存在'}) @@ -195,8 +224,8 @@ class ModifyRolePermissionView(APIView): if action == 'delete_role': # 删除角色:同时删除角色权限关联、用户角色关联 with transaction.atomic(): - RolePermission.objects.filter(role=role).delete() - UserRole.objects.filter(role=role).delete() + RolePermission.query.filter(role=role).delete() + UserRole.query.filter(role=role).delete() role.delete() return Response({'code': 0, 'msg': '角色删除成功'}) @@ -208,13 +237,13 @@ class ModifyRolePermissionView(APIView): if role.role_code == '000001' and perm_code == '000001': return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'}) try: - perm = Permission.objects.get(perm_code=perm_code) + perm = Permission.query.get(perm_code=perm_code) except Permission.DoesNotExist: return Response({'code': 404, 'msg': '权限不存在'}) # 检查是否已存在 - if RolePermission.objects.filter(role=role, permission=perm).exists(): + if RolePermission.query.filter(role=role, permission=perm).exists(): return Response({'code': 400, 'msg': '该角色已拥有此权限'}) - RolePermission.objects.create(role=role, permission=perm) + RolePermission.query.create(role=role, permission=perm) return Response({'code': 0, 'msg': '权限添加成功'}) elif action == 'remove_permission': @@ -225,10 +254,10 @@ class ModifyRolePermissionView(APIView): if role.role_code == '000001' and perm_code == '000001': return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'}) try: - perm = Permission.objects.get(perm_code=perm_code) + perm = Permission.query.get(perm_code=perm_code) except Permission.DoesNotExist: return Response({'code': 404, 'msg': '权限不存在'}) - deleted = RolePermission.objects.filter(role=role, permission=perm).delete() + deleted = RolePermission.query.filter(role=role, permission=perm).delete() if deleted[0] == 0: return Response({'code': 400, 'msg': '该角色未拥有此权限'}) return Response({'code': 0, 'msg': '权限移除成功'}) @@ -260,19 +289,19 @@ class AddRoleView(APIView): return Response({'code': 403, 'msg': '您无权进行此操作'}) # 检查角色名称是否已存在 - if Role.objects.filter(role_name=role_name).exists(): + if Role.query.filter(role_name=role_name).exists(): return Response({'code': 400, 'msg': '角色名称已存在'}) # 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字) while True: random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) role_code = f"ROLE_{random_str}" - if not Role.objects.filter(role_code=role_code).exists(): + if not Role.query.filter(role_code=role_code).exists(): break # 创建角色 with transaction.atomic(): - role = Role.objects.create( + role = Role.query.create( role_code=role_code, role_name=role_name, description=description, @@ -284,8 +313,8 @@ class AddRoleView(APIView): if pc == '000001': continue try: - perm = Permission.objects.get(perm_code=pc) - RolePermission.objects.create(role=role, permission=perm) + perm = Permission.query.get(perm_code=pc) + RolePermission.query.create(role=role, permission=perm) except Permission.DoesNotExist: # 忽略不存在的权限,或记录日志 logger.warning(f"添加角色时忽略不存在的权限编码: {pc}") @@ -316,7 +345,7 @@ class GetAdminUserListView(APIView): page = int(request.data.get('page', 1)) page_size = int(request.data.get('pageSize', 20)) - users = UserMain.objects.filter( + users = UserMain.query.filter( user_type='kefu', kefu_profile__isnull=False ).select_related('kefu_profile') @@ -329,7 +358,7 @@ class GetAdminUserListView(APIView): # 关键修正:先求值为列表,再传入 phone__in if role_code: user_ids_with_role = list( - UserRole.objects.filter(role__role_code=role_code) + UserRole.query.filter(role__role_code=role_code) .values_list('account_id', flat=True) ) users = users.filter(phone__in=user_ids_with_role) @@ -340,7 +369,7 @@ class GetAdminUserListView(APIView): data_list = [] for user in page_obj: - user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') + user_roles = UserRole.query.filter(account_id=user.phone).select_related('role') roles_data = [ {'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles @@ -385,7 +414,7 @@ class GetAdminRolesView(APIView): if '000001' not in permissions: return Response({'code': 403, 'msg': '您无权访问此页面'}) - roles = Role.objects.all().values('role_code', 'role_name') + roles = Role.query.all().values('role_code', 'role_name') return Response({ 'code': 0, 'data': { @@ -421,14 +450,14 @@ class GetAdminRolesView(APIView): page_size = int(request.data.get('pageSize', 20)) # 基础查询:user_type='kefu' 且存在客服扩展表 - users = UserMain.objects.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile') + users = UserMain.query.filter(user_type='kefu', kefu_profile__isnull=False).select_related('kefu_profile') if phone: users = users.filter(phone__icontains=phone) if nicheng: users = users.filter(kefu_profile__nicheng__icontains=nicheng) if role_code: # 筛选拥有该角色的用户 - user_ids_with_role = UserRole.objects.filter(role__role_code=role_code).values_list('account_id', flat=True) + user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True) users = users.filter(phone__in=user_ids_with_role) # 排序 @@ -442,7 +471,7 @@ class GetAdminRolesView(APIView): data_list = [] for user in page_obj: # 获取用户的所有角色 - user_roles = UserRole.objects.filter(account_id=user.phone).select_related('role') + user_roles = UserRole.query.filter(account_id=user.phone).select_related('role') roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles] data_list.append({ 'phone': user.phone, @@ -488,7 +517,7 @@ class ModifyAdminUserView(APIView): return Response({'code': 400, 'msg': '缺少目标用户账号'}) try: - target_user = UserMain.objects.get(phone=target_phone, user_type='kefu') + target_user = UserMain.query.get(phone=target_phone, user_type='kefu') target_kefu = target_user.kefu_profile except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '用户不存在'}) @@ -524,10 +553,10 @@ class ModifyAdminUserView(APIView): return Response({'code': 400, 'msg': '请选择角色'}) for rc in role_codes: try: - role = Role.objects.get(role_code=rc) + role = Role.query.get(role_code=rc) except Role.DoesNotExist: continue - UserRole.objects.get_or_create(account_id=target_phone, role=role) + UserRole.query.get_or_create(account_id=target_phone, role=role) return Response({'code': 0, 'msg': '角色添加成功'}) elif action == 'remove_user_role': @@ -535,10 +564,10 @@ class ModifyAdminUserView(APIView): if not role_code: return Response({'code': 400, 'msg': '缺少角色编码'}) try: - role = Role.objects.get(role_code=role_code) + role = Role.query.get(role_code=role_code) except Role.DoesNotExist: return Response({'code': 404, 'msg': '角色不存在'}) - UserRole.objects.filter(account_id=target_phone, role=role).delete() + UserRole.query.filter(account_id=target_phone, role=role).delete() return Response({'code': 0, 'msg': '角色移除成功'}) else: @@ -575,25 +604,25 @@ class AddAdminUserView(APIView): return Response({'code': 400, 'msg': '密码和二级密码至少6位'}) # 校验手机号是否已被占用(且 user_type='kefu') - if UserMain.objects.filter(phone=phone, user_type='kefu').exists(): + if UserMain.query.filter(phone=phone, user_type='kefu').exists(): return Response({'code': 400, 'msg': '该账号已存在'}) # 生成唯一的 yonghuid(7位数字) while True: yonghuid = str(random.randint(1000000, 9999999)) - if not UserMain.objects.filter(yonghuid=yonghuid).exists(): + if not UserMain.query.filter(yonghuid=yonghuid).exists(): break # 生成唯一的 openid(格式:admin_{时间戳}_{随机6位}) timestamp = str(int(time.time() * 1000)) random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) openid = f"admin_{timestamp}_{random_suffix}" - while UserMain.objects.filter(openid=openid).exists(): + while UserMain.query.filter(openid=openid).exists(): random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) openid = f"admin_{timestamp}_{random_suffix}" with transaction.atomic(): - user_main = UserMain.objects.create( + user_main = UserMain.query.create( yonghuid=yonghuid, openid=openid, phone=phone, @@ -601,7 +630,7 @@ class AddAdminUserView(APIView): user_type='kefu' ) # 创建客服扩展表(后台用户扩展) - UserKefu.objects.create( + UserKefu.query.create( user=user_main, nicheng=nicheng, erjimima=erjimima, @@ -610,8 +639,8 @@ class AddAdminUserView(APIView): # 分配角色 for rc in role_codes: try: - role = Role.objects.get(role_code=rc) - UserRole.objects.create(account_id=phone, role=role) + role = Role.query.get(role_code=rc) + UserRole.query.create(account_id=phone, role=role) except Role.DoesNotExist: pass @@ -670,7 +699,7 @@ class KefuGetDashouListView(APIView): return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) # 4. 构建打手查询集(仅打手类型) - dashou_qs = UserDashou.objects.select_related('user').all() + dashou_qs = UserDashou.query.select_related('user').all() # 关键词筛选(用户ID或昵称) if keyword: @@ -685,7 +714,7 @@ class KefuGetDashouListView(APIView): # 6. 统计有效打手数(有会员且未过期) dashou_user_ids = dashou_qs.values_list('user__yonghuid', flat=True) now = timezone.now() - valid_user_ids = Huiyuangoumai.objects.filter( + valid_user_ids = Huiyuangoumai.query.filter( yonghu_id__in=dashou_user_ids, huiyuan_zhuangtai=1, daoqi_time__gt=now @@ -757,7 +786,7 @@ class KefuGetDashouDetailView(APIView): # 4. 查询打手主表和扩展表 try: - user_main = UserMain.objects.select_related('dashou_profile').get( + user_main = UserMain.query.select_related('dashou_profile').get( yonghuid=uid ) except UserMain.DoesNotExist: @@ -795,7 +824,7 @@ class KefuGetDashouDetailView(APIView): } member_list = [] # 仍然需要返回 all_members 和 permissions - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) return Response({ 'code': 0, 'data': { @@ -835,7 +864,7 @@ class KefuGetDashouDetailView(APIView): } # 6. 查询打手已拥有的会员(未过期) - member_records = Huiyuangoumai.objects.filter( + member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-create_time') member_list = [] @@ -848,7 +877,7 @@ class KefuGetDashouDetailView(APIView): if not is_expired: try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) huiyuan_name = huiyuan.jieshao except Huiyuan.DoesNotExist: huiyuan_name = '未知会员' @@ -861,7 +890,7 @@ class KefuGetDashouDetailView(APIView): }) # 7. 查询所有会员类型(用于前端添加会员) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao')) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) return Response({ 'code': 0, @@ -914,7 +943,7 @@ class KefuGetDashouDetailView(APIView): # 3. 查询打手主表和扩展表 try: # 使用 select_related 预加载 dashou_profile,减少数据库查询 - user_main = UserMain.objects.select_related('dashou_profile').get( + user_main = UserMain.query.select_related('dashou_profile').get( yonghuid=uid ) except UserMain.DoesNotExist: @@ -990,7 +1019,7 @@ class KefuGetDashouDetailView(APIView): } # 5. 查询打手会员信息(仅未过期) - member_records = Huiyuangoumai.objects.filter( + member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-create_time') @@ -1006,7 +1035,7 @@ class KefuGetDashouDetailView(APIView): if not is_expired: # 获取会员详情 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) huiyuan_name = huiyuan.jieshao except Huiyuan.DoesNotExist: huiyuan_name = '未知会员' @@ -1064,7 +1093,7 @@ class KefuUpdateDashouView(APIView): # 3. 查询打手 try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) @@ -1093,7 +1122,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yue -= amount dashou_profile.save() after = dashou_profile.yue - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1109,7 +1138,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yue += amount dashou_profile.save() after = dashou_profile.yue - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1128,7 +1157,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin -= amount dashou_profile.save() after = dashou_profile.yajin - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1144,7 +1173,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin += amount dashou_profile.save() after = dashou_profile.yajin - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1163,7 +1192,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen -= jifen dashou_profile.save() after = dashou_profile.jifen - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1179,7 +1208,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen += jifen dashou_profile.save() after = dashou_profile.jifen - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1197,7 +1226,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1213,7 +1242,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1229,7 +1258,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1245,7 +1274,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '缺少会员ID'}) days = int(value) if value else 30 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员类型不存在'}) record, created = Huiyuangoumai.objects.select_for_update().get_or_create( @@ -1262,7 +1291,7 @@ class KefuUpdateDashouView(APIView): record.daoqi_time = new_daoqi record.huiyuan_zhuangtai = 1 record.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1278,11 +1307,11 @@ class KefuUpdateDashouView(APIView): if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) try: - record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) except Huiyuangoumai.DoesNotExist: return Response({'code': 404, 'msg': '该打手未购买此会员'}) record.delete() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1301,7 +1330,7 @@ class KefuUpdateDashouView(APIView): # 可限制合法值,这里只记录日志 dashou_profile.chenghao = new_chenghao dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -1380,7 +1409,7 @@ class KefuGetShangjiaListView(APIView): return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) # 4. 构建商家查询集(商家扩展表 + 关联用户主表) - shangjia_qs = UserShangjia.objects.select_related('user').all() + shangjia_qs = UserShangjia.query.select_related('user').all() # 关键词筛选(用户ID或昵称) if keyword: @@ -1496,7 +1525,7 @@ class KefuGetShangjiaDetailView(APIView): # 查询商家 try: - shangjia = UserShangjia.objects.select_related('user').get(user__yonghuid=yonghuid) + shangjia = UserShangjia.query.select_related('user').get(user__yonghuid=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) @@ -1652,11 +1681,11 @@ class GetProductBaseDataView(APIView): return Response({'code': 403, 'msg': '无权限管理商品'}) # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_qs = ShangpinLeixing.query.all().order_by('paixu') type_list = [] for t in type_qs: # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.objects.filter( + product_count = Shangpin.query.filter( leixing_id=t.id, dianpu__isnull=True, # 所属店铺为空 dianpu_leixing__isnull=True # 店铺商品类型为空 @@ -1671,11 +1700,11 @@ class GetProductBaseDataView(APIView): }) # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') zone_list = [] for z in zone_qs: # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) - product_count = Shangpin.objects.filter( + product_count = Shangpin.query.filter( zhuanqu_id=z.id, dianpu__isnull=True, # 所属店铺为空 dianpu_leixing__isnull=True # 店铺商品类型为空 @@ -1690,7 +1719,7 @@ class GetProductBaseDataView(APIView): }) # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) return Response({ 'code': 0, @@ -1758,7 +1787,7 @@ class GetProductListView(APIView): return Response({'code': 400, 'msg': '分页参数错误'}) # 🔥 修改:构建查询集,增加公共商品筛选(店铺为空且店铺商品类型为空) - qs = Shangpin.objects.filter( + qs = Shangpin.query.filter( dianpu__isnull=True, # 所属店铺为空 dianpu_leixing__isnull=True # 店铺商品类型为空 ) @@ -1867,11 +1896,11 @@ class GetProductListView(APIView): return Response({'code': 403, 'msg': '无权限管理商品'}) # 获取所有商品类型(按 paixu 升序) - type_qs = ShangpinLeixing.objects.all().order_by('paixu') + type_qs = ShangpinLeixing.query.all().order_by('paixu') type_list = [] for t in type_qs: # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) - product_count = Shangpin.objects.filter(leixing_id=t.id).count() + product_count = Shangpin.query.filter(leixing_id=t.id).count() type_list.append({ 'id': t.id, 'jieshao': t.jieshao or '', @@ -1882,10 +1911,10 @@ class GetProductListView(APIView): }) # 获取所有专区(按 paixu 升序) - zone_qs = ShangpinZhuanqu.objects.all().order_by('paixu') + zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') zone_list = [] for z in zone_qs: - product_count = Shangpin.objects.filter(zhuanqu_id=z.id).count() + product_count = Shangpin.query.filter(zhuanqu_id=z.id).count() zone_list.append({ 'id': z.id, 'mingzi': z.mingzi or '', @@ -1896,7 +1925,7 @@ class GetProductListView(APIView): }) # 获取所有会员(只需 ID、介绍、价格) - member_list = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage')) + member_list = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage')) return Response({ 'code': 0, @@ -1963,7 +1992,7 @@ class GetProductListView(APIView): return Response({'code': 400, 'msg': '分页参数错误'}) # 构建查询集 - qs = Shangpin.objects.all() + qs = Shangpin.query.all() if leixing_id: try: @@ -2089,7 +2118,7 @@ class SaveProductOrderView(APIView): if sort_type == 'leixing': # 更新商品类型的排序 for item in items: - ShangpinLeixing.objects.filter(id=item['id']).update(paixu=item['paixu']) + ShangpinLeixing.query.filter(id=item['id']).update(paixu=item['paixu']) else: # zhuanqu leixing_id = request.data.get('leixing_id') if not leixing_id: @@ -2100,7 +2129,7 @@ class SaveProductOrderView(APIView): return Response({'code': 400, 'msg': 'leixing_id格式错误'}) # 可选:检查该专区是否属于该类型,但不强制,由前端保证 for item in items: - ShangpinZhuanqu.objects.filter(id=item['id']).update(paixu=item['paixu']) + ShangpinZhuanqu.query.filter(id=item['id']).update(paixu=item['paixu']) return Response({'code': 0, 'msg': '排序保存成功'}) @@ -2180,11 +2209,11 @@ class AddProductView(APIView): # 校验类型和专区的存在性及关联性 try: - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '商品类型不存在'}) try: - zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) if zhuanqu_obj.leixing_id != leixing_id: return Response({'code': 400, 'msg': '专区不属于该商品类型'}) except ShangpinZhuanqu.DoesNotExist: @@ -2203,7 +2232,7 @@ class AddProductView(APIView): else: huiyuan_id_val = huiyuan_id # 验证会员是否存在 - if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id_val).exists(): + if not Huiyuan.query.filter(huiyuan_id=huiyuan_id_val).exists(): return Response({'code': 400, 'msg': '指定的会员不存在'}) else: # 佣金要求 @@ -2255,7 +2284,7 @@ class AddProductView(APIView): # 创建商品 try: with transaction.atomic(): - product = Shangpin.objects.create( + product = Shangpin.query.create( biaoqian=biaoqian, jiage=jiage, kucun=kucun, @@ -2326,7 +2355,7 @@ class DeleteProductView(APIView): # 查询商品是否存在 try: - product = Shangpin.objects.get(id=product_id) + product = Shangpin.query.get(id=product_id) except Shangpin.DoesNotExist: return Response({'code': 404, 'msg': '商品不存在'}) @@ -2388,18 +2417,18 @@ class GetProductDetailView(APIView): # 查询商品 try: - product = Shangpin.objects.get(id=product_id) + product = Shangpin.query.get(id=product_id) except Shangpin.DoesNotExist: return Response({'code': 404, 'msg': '商品不存在'}) # 查询所有商品类型、专区、会员(用于前端下拉选择) - type_list = list(ShangpinLeixing.objects.all().values( + type_list = list(ShangpinLeixing.query.all().values( 'id', 'jieshao', 'tupian_url', 'shenhezhuangtai' )) - zone_list = list(ShangpinZhuanqu.objects.all().values( + zone_list = list(ShangpinZhuanqu.query.all().values( 'id', 'mingzi', 'leixing_id', 'shenhezhuangtai' )) - member_list = list(Huiyuan.objects.all().values( + member_list = list(Huiyuan.query.all().values( 'huiyuan_id', 'jieshao', 'jiage' )) @@ -2521,9 +2550,9 @@ class UpdateProductView(APIView): shenhezhuangtai = 1 # 校验类型和专区存在性 - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + if not ShangpinLeixing.query.filter(id=leixing_id).exists(): return Response({'code': 400, 'msg': '商品类型不存在'}) - if not ShangpinZhuanqu.objects.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): + if not ShangpinZhuanqu.query.filter(id=zhuanqu_id, leixing_id=leixing_id).exists(): return Response({'code': 400, 'msg': '专区不存在或不属于该类型'}) # ---------- 2. 会员/佣金 ---------- @@ -2531,7 +2560,7 @@ class UpdateProductView(APIView): yongjin = None if yaoqiuleixing == 1: huiyuan_id = request.data.get('huiyuan_id', '').strip() or None - if huiyuan_id and not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): + if huiyuan_id and not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): return Response({'code': 400, 'msg': '指定的会员不存在'}) else: try: @@ -2642,7 +2671,7 @@ class GetMemberListView(APIView): if '3300a' not in permissions: return Response({'code': 403, 'msg': '无权限查看会员列表'}) - members = Huiyuan.objects.all().order_by('-create_time') + members = Huiyuan.query.all().order_by('-create_time') member_list = [] for m in members: member_list.append({ @@ -2771,12 +2800,12 @@ class AddMemberView(APIView): def generate_huiyuan_id(): while True: new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - if not Huiyuan.objects.filter(huiyuan_id=new_id).exists(): + if not Huiyuan.query.filter(huiyuan_id=new_id).exists(): return new_id with transaction.atomic(): new_id = generate_huiyuan_id() - member = Huiyuan.objects.create( + member = Huiyuan.query.create( huiyuan_id=new_id, jieshao=jieshao, jiage=jiage, @@ -2839,7 +2868,7 @@ class GetGuanliListView(APIView): status = request.data.get('status') # 构建查询:从 UserMain 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称) - qs = UserMain.objects.filter( + qs = UserMain.query.filter( guanshi_profile__isnull=False # 确保是管事 ).select_related('guanshi_profile').select_related('boss_profile') @@ -2968,7 +2997,7 @@ class GetGuanliDetailView(APIView): # 查询用户主表和管事扩展表 try: - user = UserMain.objects.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) + user = UserMain.query.select_related('guanshi_profile', 'boss_profile').get(yonghuid=yonghuid) guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except UserMain.DoesNotExist: @@ -3008,20 +3037,20 @@ class GetGuanliDetailView(APIView): permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0' # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc')) for m in all_members: m['jiage'] = str(m['jiage']) m['guanshifc'] = str(m['guanshifc']) # 获取多次分红配置(按次数和会员组织) # 查询该管事的所有多次分红记录 - duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } for record in duoci_records: # 获取会员信息 - member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() if not member: continue member_info = { @@ -3089,7 +3118,7 @@ class UpdateGuanliView(APIView): # 查询管事对象(不在事务外加锁,事务内加锁) try: - user = UserMain.objects.get(yonghuid=yonghuid) + user = UserMain.query.get(yonghuid=yonghuid) guanshi = user.guanshi_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except UserMain.DoesNotExist: @@ -3117,7 +3146,7 @@ class UpdateGuanliView(APIView): # 处理昵称(在老板扩展表中) if nickname is not None: if not boss: - boss = UserBoss.objects.create(user=user, nickname=nickname) + boss = UserBoss.query.create(user=user, nickname=nickname) else: boss.nickname = nickname boss.save() @@ -3196,7 +3225,7 @@ class UpdateGuanliView(APIView): # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): # 获取当前所有首次定制记录 - existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1) # 前端传来的定制字典 {会员ID: 金额} for huiyuan_id, amount in custom_first.items(): try: @@ -3205,9 +3234,9 @@ class UpdateGuanliView(APIView): continue if amount <= 0: # 金额为0或负数表示删除定制 - DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: - DuociFenhong.objects.update_or_create( + DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, @@ -3246,7 +3275,7 @@ class UpdateGuanliView(APIView): continue keep_set.add((cishu, huiyuan_id)) # 更新或创建 - DuociFenhong.objects.update_or_create( + DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, @@ -3256,7 +3285,7 @@ class UpdateGuanliView(APIView): } ) # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() @@ -3295,37 +3324,37 @@ class GetWithdrawSettingsView(APIView): # 1. 手续费利率(Lilubiao:5打手/6管事/8组长/9审核官/10商家/11打手押金) rate_map = {} for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]: - obj = Lilubiao.objects.filter(fadanpingtai=code).first() + obj = Lilubiao.query.filter(fadanpingtai=code).first() rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 # 1b. 订单/押金分红费率(Lilubiao:1平台订单/3商家订单/12押金管事/13商家订单管事分红) order_rate_map = {} for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]: - obj = Lilubiao.objects.filter(fadanpingtai=code).first() + obj = Lilubiao.query.filter(fadanpingtai=code).first() order_rate_map[leixing] = float(obj.lilu) if obj and obj.lilu is not None else 0.0 # 2. 个人每日限额 (leixing=1,2,3) quota_map = {} for leixing in [1, 2, 3]: - obj = TixianQuotaDefault.objects.filter(leixing=leixing).first() + obj = TixianQuotaDefault.query.filter(leixing=leixing).first() quota_map[leixing] = float(obj.default_quota) if obj else 20.0 # 3. 每日总限额(提现类型1~6 → 配置表4~9) total_limit_map = {} for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]: - obj = TixianQuotaDefault.objects.filter(leixing=quota_leixing).first() + obj = TixianQuotaDefault.query.filter(leixing=quota_leixing).first() total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0 # 4. 当日已提现总额(按提现类型 leixing 1~6) today = date.today() today_totals = {i: 0.0 for i in range(1, 7)} - records = TixianRiTongji.objects.filter(riqi=today) + records = TixianRiTongji.query.filter(riqi=today) for rec in records: if rec.leixing in today_totals: today_totals[rec.leixing] = float(rec.total_amount) # 🆕 获取提现模式(自动1 / 手动2),默认2 - config = WithdrawConfig.objects.filter(id=1).first() + config = WithdrawConfig.query.filter(id=1).first() withdraw_mode = config.mode if config else 2 return Response({ @@ -3476,7 +3505,7 @@ class UpdateWithdrawSettingsView(APIView): if not any(p in permissions for p in ('5500a', '5500b', '5500c')): return Response({'code': 403, 'msg': '无权限修改提现模式'}) # 更新或创建 id=1 的配置 - WithdrawConfig.objects.update_or_create( + WithdrawConfig.query.update_or_create( id=1, defaults={'mode': int(withdraw_mode)} ) @@ -3537,7 +3566,7 @@ class GetZuzhangListView(APIView): commission_value = request.data.get('commission_value') # 5. 构建基础 QuerySet(预加载关联表) - queryset = UserZuzhang.objects.select_related('user', 'user__boss_profile').all() + queryset = UserZuzhang.query.select_related('user', 'user__boss_profile').all() # 6. 应用筛选条件(使用 ORM 安全过滤) if keyword: @@ -3660,7 +3689,7 @@ class GetZuzhangDetailView(APIView): # 查询组长主表、扩展表、老板扩展表(昵称) try: - user = UserMain.objects.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) + user = UserMain.query.select_related('zuzhang_profile', 'boss_profile').get(yonghuid=yonghuid) zuzhang = user.zuzhang_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except UserMain.DoesNotExist: @@ -3700,19 +3729,19 @@ class GetZuzhangDetailView(APIView): permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0' # 获取所有会员(用于前端选择) - all_members = list(Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) + all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc')) for m in all_members: m['jiage'] = str(m['jiage']) m['zuzhangfc'] = str(m['zuzhangfc']) # 获取多次分红配置(按次数和会员组织) - duoci_records = DuociFenhong.objects.filter(yonghuid=yonghuid).order_by('cishu') + duoci_records = DuociFenhong.query.filter(yonghuid=yonghuid).order_by('cishu') custom_first = {} # 首次分红定制(cishu=1) extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } for record in duoci_records: # 获取会员信息 - member = Huiyuan.objects.filter(huiyuan_id=record.huiyuan).first() + member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() if not member: continue member_info = { @@ -3783,7 +3812,7 @@ class UpdateZuzhangView(APIView): # 查询组长对象 try: - user = UserMain.objects.get(yonghuid=yonghuid) + user = UserMain.query.get(yonghuid=yonghuid) zuzhang = user.zuzhang_profile boss = user.boss_profile if hasattr(user, 'boss_profile') else None except UserMain.DoesNotExist: @@ -3814,7 +3843,7 @@ class UpdateZuzhangView(APIView): pass if nickname: if not boss: - boss = UserBoss.objects.create(user=user, nickname=nickname) + boss = UserBoss.query.create(user=user, nickname=nickname) else: boss.nickname = nickname boss.save() @@ -3879,7 +3908,7 @@ class UpdateZuzhangView(APIView): # 4.2 首次定制分红(cishu=1) if custom_first is not None and isinstance(custom_first, dict): - existing_first = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu=1) + existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1) for huiyuan_id, amount in custom_first.items(): try: amount = Decimal(str(amount)) @@ -3887,9 +3916,9 @@ class UpdateZuzhangView(APIView): continue if amount <= 0: # 删除定制(恢复默认) - DuociFenhong.objects.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() + DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete() else: - DuociFenhong.objects.update_or_create( + DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, @@ -3926,7 +3955,7 @@ class UpdateZuzhangView(APIView): except: continue keep_set.add((cishu, huiyuan_id)) - DuociFenhong.objects.update_or_create( + DuociFenhong.query.update_or_create( yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=cishu, @@ -3936,7 +3965,7 @@ class UpdateZuzhangView(APIView): } ) # 删除不在 keep_set 中的额外次数记录 - existing_extra = DuociFenhong.objects.filter(yonghuid=yonghuid, cishu__gte=2) + existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2) for record in existing_extra: if (record.cishu, record.huiyuan) not in keep_set: record.delete() @@ -3978,11 +4007,11 @@ class GetProductTypeZoneView(APIView): return Response({'code': 403, 'msg': '您无权访问此页面'}) # 查询所有商品类型(含专区数量) - types = ShangpinLeixing.objects.all().order_by('-paixu') # paixu 越大越靠前 + types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前 type_list = [] for t in types: # 统计该类型下的专区数量 - zone_count = ShangpinZhuanqu.objects.filter(leixing_id=t.id).count() + zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count() type_list.append({ 'id': t.id, 'jieshao': t.jieshao or '', @@ -3996,7 +4025,7 @@ class GetProductTypeZoneView(APIView): }) # 查询所有专区 - zones = ShangpinZhuanqu.objects.all().order_by('-paixu') + zones = ShangpinZhuanqu.query.all().order_by('-paixu') zone_list = [ { 'id': z.id, @@ -4009,7 +4038,7 @@ class GetProductTypeZoneView(APIView): ] # 查询所有会员(只返回 id、介绍、价格) - members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao', 'jiage') + members = Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage') member_list = [ { 'huiyuan_id': m['huiyuan_id'], @@ -4127,7 +4156,7 @@ class ModifyProductTypeZoneView(APIView): # 创建类型 try: with transaction.atomic(): - leixing = ShangpinLeixing.objects.create( + leixing = ShangpinLeixing.query.create( jieshao=jieshao, tupian_url=oss_path, yaoqiuleixing=yaoqiuleixing, @@ -4147,7 +4176,7 @@ class ModifyProductTypeZoneView(APIView): if not type_id: return Response({'code': 400, 'msg': '缺少类型ID'}) try: - leixing = ShangpinLeixing.objects.get(id=type_id) + leixing = ShangpinLeixing.query.get(id=type_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 404, 'msg': '类型不存在'}) @@ -4237,7 +4266,7 @@ class ModifyProductTypeZoneView(APIView): if 'yongjin' in changes: product_updates['yongjin'] = changes['yongjin'] if product_updates: - Shangpin.objects.filter(leixing_id=type_id).update(**product_updates) + Shangpin.query.filter(leixing_id=type_id).update(**product_updates) # 删除旧图片 if image_file and old_image_path: @@ -4257,24 +4286,24 @@ class ModifyProductTypeZoneView(APIView): delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] try: - leixing = ShangpinLeixing.objects.get(id=type_id) + leixing = ShangpinLeixing.query.get(id=type_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 404, 'msg': '类型不存在'}) with transaction.atomic(): # 删除该类型下的所有专区 - zones = ShangpinZhuanqu.objects.filter(leixing_id=type_id) + zones = ShangpinZhuanqu.query.filter(leixing_id=type_id) if delete_products: # 删除专区下的所有商品 for zone in zones: - Shangpin.objects.filter(zhuanqu_id=zone.id).delete() + Shangpin.query.filter(zhuanqu_id=zone.id).delete() zones.delete() # 删除直接关联该类型的商品 - Shangpin.objects.filter(leixing_id=type_id).delete() + Shangpin.query.filter(leixing_id=type_id).delete() else: zones.delete() # 仅清除商品的类型关联 - Shangpin.objects.filter(leixing_id=type_id).update(leixing_id=None) + Shangpin.query.filter(leixing_id=type_id).update(leixing_id=None) leixing.delete() @@ -4304,7 +4333,7 @@ class ModifyProductTypeZoneView(APIView): try: leixing_id = int(leixing_id) - if not ShangpinLeixing.objects.filter(id=leixing_id).exists(): + if not ShangpinLeixing.query.filter(id=leixing_id).exists(): return Response({'code': 404, 'msg': '所属类型不存在'}) except (TypeError, ValueError): return Response({'code': 400, 'msg': '所属类型ID无效'}) @@ -4318,7 +4347,7 @@ class ModifyProductTypeZoneView(APIView): try: with transaction.atomic(): - zhuanqu = ShangpinZhuanqu.objects.create( + zhuanqu = ShangpinZhuanqu.query.create( mingzi=mingzi, leixing_id=leixing_id, shenhezhuangtai=shenhezhuangtai, @@ -4334,7 +4363,7 @@ class ModifyProductTypeZoneView(APIView): if not zone_id: return Response({'code': 400, 'msg': '缺少专区ID'}) try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) except ShangpinZhuanqu.DoesNotExist: return Response({'code': 404, 'msg': '专区不存在'}) @@ -4347,7 +4376,7 @@ class ModifyProductTypeZoneView(APIView): if leixing_id is not None: try: new_id = int(leixing_id) - if not ShangpinLeixing.objects.filter(id=new_id).exists(): + if not ShangpinLeixing.query.filter(id=new_id).exists(): return Response({'code': 404, 'msg': '新所属类型不存在'}) if new_id != zhuanqu.leixing_id: changes['leixing_id'] = new_id @@ -4385,15 +4414,15 @@ class ModifyProductTypeZoneView(APIView): delete_products = request.data.get('delete_products') in [True, 'true', '1', 'on'] try: - zhuanqu = ShangpinZhuanqu.objects.get(id=zone_id) + zhuanqu = ShangpinZhuanqu.query.get(id=zone_id) except ShangpinZhuanqu.DoesNotExist: return Response({'code': 404, 'msg': '专区不存在'}) with transaction.atomic(): if delete_products: - Shangpin.objects.filter(zhuanqu_id=zone_id).delete() + Shangpin.query.filter(zhuanqu_id=zone_id).delete() else: - Shangpin.objects.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) + Shangpin.query.filter(zhuanqu_id=zone_id).update(zhuanqu_id=None) zhuanqu.delete() return Response({'code': 0, 'msg': '删除成功'}) @@ -4423,8 +4452,8 @@ class GetRateView(APIView): return Response({'code': 403, 'msg': '您无权查看费率'}) try: - platform_rate_obj = Lilubiao.objects.get(fadanpingtai='1') - merchant_rate_obj = Lilubiao.objects.get(fadanpingtai='3') + platform_rate_obj = Lilubiao.query.get(fadanpingtai='1') + merchant_rate_obj = Lilubiao.query.get(fadanpingtai='3') except Lilubiao.DoesNotExist as e: logger.error(f"费率记录缺失: {e}") return Response({'code': 500, 'msg': '费率配置缺失'}) @@ -4520,7 +4549,7 @@ class PopupNoticeListAPIView(APIView): return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'}) # 4. 查询所有页面,并预加载关联的弹窗和图片(避免N+1查询) - pages = PopupPage.objects.all().prefetch_related('popups__images').order_by('id') + pages = PopupPage.query.all().prefetch_related('popups__images').order_by('id') # 5. 序列化输出 serializer = PopupPageSerializer(pages, many=True) @@ -4585,10 +4614,10 @@ class PopupNoticeModifyAPIView(APIView): if not data.get('page_key') or not data.get('name'): return Response({'code': 400, 'msg': '缺少 page_key 或 name'}) # 唯一性校验 - if PopupPage.objects.filter(page_key=data['page_key']).exists(): + if PopupPage.query.filter(page_key=data['page_key']).exists(): return Response({'code': 400, 'msg': '页面标识已存在'}) - page = PopupPage.objects.create( + page = PopupPage.query.create( page_key=data['page_key'], name=data['name'], description=data.get('description', ''), @@ -4603,7 +4632,7 @@ class PopupNoticeModifyAPIView(APIView): if not page_id: return Response({'code': 400, 'msg': '缺少页面id'}) try: - page = PopupPage.objects.get(id=page_id) + page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) @@ -4623,7 +4652,7 @@ class PopupNoticeModifyAPIView(APIView): if not page_id: return Response({'code': 400, 'msg': '缺少页面id'}) try: - page = PopupPage.objects.get(id=page_id) + page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) @@ -4656,15 +4685,15 @@ class PopupNoticeModifyAPIView(APIView): if not page_id or not popup_id: return Response({'code': 400, 'msg': '缺少 page_id 或 popup_id'}) try: - page = PopupPage.objects.get(id=page_id) + page = PopupPage.query.get(id=page_id) except PopupPage.DoesNotExist: return Response({'code': 404, 'msg': '页面不存在'}) # 同一页面下 popup_id 必须唯一 - if PopupConfig.objects.filter(page=page, popup_id=popup_id).exists(): + if PopupConfig.query.filter(page=page, popup_id=popup_id).exists(): return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'}) - popup = PopupConfig.objects.create( + popup = PopupConfig.query.create( page=page, popup_id=popup_id, title=data.get('title', ''), @@ -4687,7 +4716,7 @@ class PopupNoticeModifyAPIView(APIView): if not popup_id: return Response({'code': 400, 'msg': '缺少弹窗id'}) try: - popup = PopupConfig.objects.get(id=popup_id) + popup = PopupConfig.query.get(id=popup_id) except PopupConfig.DoesNotExist: return Response({'code': 404, 'msg': '弹窗不存在'}) @@ -4713,7 +4742,7 @@ class PopupNoticeModifyAPIView(APIView): if not popup_id: return Response({'code': 400, 'msg': '缺少弹窗id'}) try: - popup = PopupConfig.objects.get(id=popup_id) + popup = PopupConfig.query.get(id=popup_id) except PopupConfig.DoesNotExist: return Response({'code': 404, 'msg': '弹窗不存在'}) @@ -4758,7 +4787,7 @@ class PopupNoticeModifyAPIView(APIView): if not popup_id: return Response({'code': 400, 'msg': '缺少 popup_id'}) try: - popup = PopupConfig.objects.get(id=popup_id) + popup = PopupConfig.query.get(id=popup_id) except PopupConfig.DoesNotExist: return Response({'code': 404, 'msg': '弹窗不存在'}) @@ -4774,7 +4803,7 @@ class PopupNoticeModifyAPIView(APIView): return Response({'code': 400, 'msg': str(e)}) # 保存图片记录 - image = PopupImage.objects.create( + image = PopupImage.query.create( popup_config=popup, image_url=relative_path, text=request.data.get('text', ''), @@ -4788,7 +4817,7 @@ class PopupNoticeModifyAPIView(APIView): if not image_id: return Response({'code': 400, 'msg': '缺少图片id'}) try: - image = PopupImage.objects.get(id=image_id) + image = PopupImage.query.get(id=image_id) except PopupImage.DoesNotExist: return Response({'code': 404, 'msg': '图片不存在'}) @@ -4818,7 +4847,7 @@ class PopupNoticeModifyAPIView(APIView): if not image_id: return Response({'code': 400, 'msg': '缺少图片id'}) try: - image = PopupImage.objects.get(id=image_id) + image = PopupImage.query.get(id=image_id) except PopupImage.DoesNotExist: return Response({'code': 404, 'msg': '图片不存在'}) @@ -4857,7 +4886,7 @@ class ShopListView(APIView): zhengqu_zonge_max = request.data.get('zhengqu_zonge_max') # 4. 构建查询集(先查店铺,预加载关联凭证) - queryset = Dianpu.objects.select_related('pingzheng').all() + queryset = Dianpu.query.select_related('pingzheng').all() if dianpu_mingcheng: queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) @@ -4906,7 +4935,7 @@ class ShopListView(APIView): }) # 获取默认配置(ID=1,不存在则创建) - default_config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1, defaults={ + default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={ 'moren_choucheng_feilv': 0.1, 'moren_tixian_xiane': 1000.00 }) @@ -5026,24 +5055,24 @@ class ShopModifyView(APIView): return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'}) # 检查用户是否存在 - if not UserMain.objects.filter(yonghuid=yonghu_id).exists(): + if not UserMain.query.filter(yonghuid=yonghu_id).exists(): return Response({'code': 404, 'msg': '用户不存在'}) # 检查凭证是否已存在 - if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): + if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): return Response({'code': 400, 'msg': '登录账号已存在'}) - if YonghuPingzheng.objects.filter(yonghu=yonghu_id).exists(): + if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists(): return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'}) try: with transaction.atomic(): - pingzheng = YonghuPingzheng.objects.create( + pingzheng = YonghuPingzheng.query.create( yonghu=yonghu_id, zhanghao=zhanghao, mima=mima, is_active=True ) - dianpu = Dianpu.objects.create( + dianpu = Dianpu.query.create( pingzheng=pingzheng, dianpu_mingcheng=dianpu_mingcheng, lianxi_dianhua=request.data.get('lianxi_dianhua', ''), @@ -5074,7 +5103,7 @@ class ShopModifyView(APIView): return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'}) try: - config, _ = DianpuMorenPeizhi.objects.get_or_create(id=1) + config, _ = DianpuMorenPeizhi.query.get_or_create(id=1) if modify_commission: config.moren_choucheng_feilv = feilv if modify_withdraw: @@ -5115,7 +5144,7 @@ class ShopPublicTypeAndAuditView(APIView): # ---------- 查询公共商品类型(仅正常审核状态的) ---------- try: # shenhezhuangtai = 1 表示正常 (根据模型默认值) - types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( + types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( 'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin' ) type_list = [] @@ -5133,7 +5162,7 @@ class ShopPublicTypeAndAuditView(APIView): return Response({'code': 500, 'msg': '服务器错误,请稍后重试'}) # ---------- 获取审核模式配置 ---------- - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False return Response({ @@ -5175,7 +5204,7 @@ class ShopListForProductView(APIView): zhuangtai = request.data.get('zhuangtai') # ---------- 构建查询集(关联凭证表获取用户ID) ---------- - shops = Dianpu.objects.select_related('pingzheng').all() + shops = Dianpu.query.select_related('pingzheng').all() # 状态筛选:只接受 0 或 1 if zhuangtai is not None: @@ -5260,7 +5289,7 @@ class ShopProductTypeMappingView(APIView): shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) # ---------- 查询(连表店铺和公共类型) ---------- - mappings = ShangpinLeixingDianpu.objects.select_related('dianpu', 'gonggong_leixing').all() + mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all() # 筛选条件:均为参数化,完全防注入 if shop_id is not None: @@ -5447,7 +5476,7 @@ class ProductListView(APIView): fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) # ----- 构建查询(预加载店铺和店铺商品类型) ----- - products = Shangpin.objects.select_related('dianpu', 'dianpu_leixing').all() + products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all() # 按店铺筛选 if shop_id is not None: @@ -5502,7 +5531,7 @@ class ProductListView(APIView): type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None} public_type_map = {} if type_ids: - public_types = ShangpinLeixing.objects.filter(id__in=type_ids).values('id', 'jieshao') + public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao') public_type_map = {t['id']: t['jieshao'] for t in public_types} product_list = [] @@ -5707,7 +5736,7 @@ class FaKuanTongJiView(APIView): if not has_fadan_view_permission(permissions, username): return Response({'code': 403, 'msg': '无权限查看罚款统计'}) - stats = Fadan.objects.aggregate( + stats = Fadan.query.aggregate( total=Count('id'), daijiaona=Count('id', filter=Q(zhuangtai=1)), shensuzhong=Count('id', filter=Q(zhuangtai=3)), @@ -5746,7 +5775,7 @@ class FaKuanLieBiaoView(APIView): if page_size > 100: page_size = 100 - qs = Fadan.objects.all() + qs = Fadan.query.all() # 状态筛选 zhuangtai = request.data.get('zhuangtai') @@ -5789,7 +5818,7 @@ class FaKuanLieBiaoView(APIView): zhengju_map = {} shensu_map = {} if fadan_ids: - tupians = FadanShensuTupian.objects.filter( + tupians = FadanShensuTupian.query.filter( fadan_id__in=fadan_ids ).values('fadan_id', 'tupian_url', 'yongtu') for t in tupians: @@ -5909,7 +5938,7 @@ class FaKuanChuLiView(APIView): # 保存图片(证据图片,用途1) for relative_url in uploaded_urls: - FadanShensuTupian.objects.create( + FadanShensuTupian.query.create( fadan_id=fadan_id, beichufa_id=fadan.beichufa_id, tupian_url=relative_url, @@ -5962,7 +5991,7 @@ class FaKuanChuangJianView(APIView): # 验证用户是否存在对应的扩展表 try: - user = UserMain.objects.get(yonghuid=beichufa_id) + user = UserMain.query.get(yonghuid=beichufa_id) profile_attr = SHENFEN_PROFILE_MAP.get(shenfen) if not profile_attr or not hasattr(user, profile_attr): return Response({'code': 400, 'msg': '该用户不是所选身份'}) @@ -5989,7 +6018,7 @@ class FaKuanChuangJianView(APIView): uploaded_urls.append(file_path) with transaction.atomic(): - fadan = Fadan.objects.create( + fadan = Fadan.query.create( beichufa_id=beichufa_id, shenfen=shenfen, shenqing_chufa=request.user.phone, # 申请人(客服)手机号 @@ -6000,7 +6029,7 @@ class FaKuanChuangJianView(APIView): zhuangtai=1, # 待缴纳 ) for relative_url in uploaded_urls: - FadanShensuTupian.objects.create( + FadanShensuTupian.query.create( fadan=fadan, beichufa_id=beichufa_id, tupian_url=relative_url, @@ -6015,6 +6044,181 @@ class FaKuanChuangJianView(APIView): return Response({'code': 500, 'msg': '系统繁忙'}) +class FineApplyView(APIView): + """ + 客服罚款申请接口(基于订单对打手发起罚款) + POST /houtai/kffkdssq + 权限: JWT认证 + 客服权限验证 + + 请求参数: + username (必填) : 客服用户名(手机号) + dingdan_id (必填) : 订单ID + chufaliyou (必填) : 罚款原因 + fakuanjine (必填) : 罚款金额(元) + yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1 + """ + permission_classes = [IsAuthenticated] + parser_classes = [MultiPartParser, FormParser, JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + chufaliyou = request.data.get('chufaliyou', '').strip() + fakuanjine = request.data.get('fakuanjine') + yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) + + if not all([username, dingdan_id, chufaliyou, fakuanjine]): + return Response({'code': 400, 'msg': '参数不完整'}) + + try: + fakuanjine = float(fakuanjine) + except (ValueError, TypeError): + return Response({'code': 400, 'msg': '罚款金额格式错误'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 验证订单存在 + try: + order = Dingdan.query.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + # 获取打手ID + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) + + # 重复罚款检查 + if Fadan.query.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + return Response({'code': 400, 'msg': '该打手已被罚款过'}) + + # 检查权限(打手身份 shenfen=1) + if not check_fadan_permission(permissions, 1): + return Response({'code': 403, 'msg': '无权限处罚打手'}) + + # 处理图片上传 + uploaded_urls = [] + files = request.FILES.getlist('tupian') or [] + if not files: + single = request.FILES.get('tupian') + if single: + files = [single] + for file in files: + valid, msg = validate_image(file) + if not valid: + return Response({'code': 400, 'msg': msg}) + ext = file.name.split('.')[-1] if '.' in file.name else 'jpg' + file_name = f"chufa_{int(time.time() * 1000)}.{ext}" + file_path = f"chufatupian/kechufatupian/{file_name}" + url = upload_to_oss(file, file_path) + if not url: + return Response({'code': 500, 'msg': '图片上传失败'}) + uploaded_urls.append(file_path) + + # 创建罚单 + with transaction.atomic(): + fadan = Fadan.query.create( + beichufa_id=dashou_id, + shenqing_chufa=request.user.phone, + shenfen=1, # 被处罚人身份:1 打手 + chufaliyou=chufaliyou, + fakuanjine=fakuanjine, + guanliandingdan_id=dingdan_id, + zhuangtai=1, # 待缴纳 + yingxiang_qiangdan=yingxiang_qiangdan, + shenqingren_shenfen=1, # 申请人身份:1 客服 + ) + for relative_url in uploaded_urls: + FadanShensuTupian.query.create( + fadan=fadan, + beichufa_id=dashou_id, + tupian_url=relative_url, + yongtu=1 + ) + + logger.info(f"客服罚款申请成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") + return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': fadan.id}}) + + except Exception as e: + logger.error(f"客服罚款申请异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) + + +class PunishDashouView(APIView): + """ + 客服处罚打手接口(扣除积分) + POST /houtai/kpcf + 权限: JWT认证 + 客服权限验证 + + 请求参数: + username (必填) : 客服用户名(手机号) + dingdan_id (必填) : 订单ID + reason (可选) : 处罚原因 + """ + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + def post(self, request): + try: + username = request.data.get('username', '').strip() + dingdan_id = request.data.get('dingdan_id', '').strip() + reason = request.data.get('reason', '').strip() + + if not all([username, dingdan_id]): + return Response({'code': 400, 'msg': '参数不完整'}) + + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return permissions + + # 检查权限(打手身份 shenfen=1) + if not check_fadan_permission(permissions, 1): + return Response({'code': 403, 'msg': '无权限处罚打手'}) + + # 查询订单 + try: + order = Dingdan.query.get(dingdan_id=dingdan_id) + except Dingdan.DoesNotExist: + return Response({'code': 404, 'msg': '订单不存在'}) + + # 获取打手ID + dashou_id = order.jiedan_dashou_id + if not dashou_id: + return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}) + + # 查询打手 + try: + dashou_user = UserMain.query.get(yonghuid=dashou_id) + dashou = dashou_user.dashou_profile + except UserMain.DoesNotExist: + return Response({'code': 404, 'msg': '打手用户不存在'}) + except ObjectDoesNotExist: + return Response({'code': 400, 'msg': '该用户不是打手'}) + + # 创建处罚记录(扣除积分5分) + jifen = 5 + with transaction.atomic(): + chufa = Chufajilu.query.create( + dashouid=dashou_id, + qingqiuid=username, + cfliyou=reason or '', + sqzhuangtai=0, + jifen=jifen, + dingdan_id=dingdan_id, + ) + # 扣除打手积分 + dashou.jifen = F('jifen') - jifen + dashou.save(update_fields=['jifen']) + + logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {dingdan_id}, 扣除积分 {jifen}") + return Response({'code': 0, 'msg': '处罚成功', 'data': {'chufa_id': chufa.id}}) + + except Exception as e: + logger.error(f"客服处罚打手异常: {traceback.format_exc()}") + return Response({'code': 500, 'msg': '系统繁忙'}) @@ -6048,7 +6252,7 @@ class HqbkxxView(APIView): # 5. 查询所有板块 bankuai_list = [] - for bk in Bankuai.objects.all(): + for bk in Bankuai.query.all(): bankuai_list.append({ 'bankuai_id': bk.bankuai_id, 'mingcheng': bk.mingcheng, @@ -6057,7 +6261,7 @@ class HqbkxxView(APIView): # 6. 查询所有商品类型 shangpin_leixing_list = [] - for sp in ShangpinLeixing.objects.all(): + for sp in ShangpinLeixing.query.all(): shangpin_leixing_list.append({ 'id': sp.id, 'jieshao': sp.jieshao, @@ -6067,7 +6271,7 @@ class HqbkxxView(APIView): # 7. 查询所有会员 huiyuan_list = [] - for hy in Huiyuan.objects.all(): + for hy in Huiyuan.query.all(): huiyuan_list.append({ 'huiyuan_id': hy.huiyuan_id, 'jieshao': hy.jieshao, @@ -6125,21 +6329,21 @@ class BkxgView(APIView): bankuai_name = request.data.get('bankuai_name', '').strip() if not bankuai_name: return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.objects.filter(mingcheng=bankuai_name).exists(): + if Bankuai.query.filter(mingcheng=bankuai_name).exists(): return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) shangpin_ids = request.data.get('shangpin_ids', []) huiyuan_ids = request.data.get('huiyuan_ids', []) with transaction.atomic(): - new_bk = Bankuai.objects.create(mingcheng=bankuai_name) + new_bk = Bankuai.query.create(mingcheng=bankuai_name) if shangpin_ids: - ShangpinLeixing.objects.filter( + ShangpinLeixing.query.filter( id__in=shangpin_ids, bankuai__isnull=True ).update(bankuai=new_bk) if huiyuan_ids: - Huiyuan.objects.filter( + Huiyuan.query.filter( huiyuan_id__in=huiyuan_ids, bankuai__isnull=True ).update(bankuai=new_bk) @@ -6156,7 +6360,7 @@ class BkxgView(APIView): return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400) try: - bk = Bankuai.objects.get(bankuai_id=bankuai_id) + bk = Bankuai.query.get(bankuai_id=bankuai_id) except Bankuai.DoesNotExist: return Response({'code': 1, 'msg': '板块不存在'}, status=404) @@ -6165,7 +6369,7 @@ class BkxgView(APIView): new_name = request.data.get('bankuai_name', '').strip() if not new_name: return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400) - if Bankuai.objects.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): + if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists(): return Response({'code': 1, 'msg': '板块名称已存在'}, status=400) bk.mingcheng = new_name bk.save() @@ -6174,8 +6378,8 @@ class BkxgView(APIView): # ==================== 删除板块 ==================== elif action == 'delete': with transaction.atomic(): - ShangpinLeixing.objects.filter(bankuai=bk).update(bankuai=None) - Huiyuan.objects.filter(bankuai=bk).update(bankuai=None) + ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None) + Huiyuan.query.filter(bankuai=bk).update(bankuai=None) bk.delete() return Response({'code': 0, 'msg': '删除成功'}) @@ -6187,12 +6391,12 @@ class BkxgView(APIView): return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) if item_type == 'shangpin_leixing': - ShangpinLeixing.objects.filter( + ShangpinLeixing.query.filter( id__in=item_ids, bankuai__isnull=True ).update(bankuai=bk) elif item_type == 'huiyuan': - Huiyuan.objects.filter( + Huiyuan.query.filter( huiyuan_id__in=item_ids, bankuai__isnull=True ).update(bankuai=bk) @@ -6208,12 +6412,12 @@ class BkxgView(APIView): return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400) if item_type == 'shangpin_leixing': - ShangpinLeixing.objects.filter( + ShangpinLeixing.query.filter( id__in=item_ids, bankuai=bk ).update(bankuai=None) elif item_type == 'huiyuan': - Huiyuan.objects.filter( + Huiyuan.query.filter( huiyuan_id__in=item_ids, bankuai=bk ).update(bankuai=None) @@ -6250,17 +6454,17 @@ class CaiwuView(APIView): today_end = today_start + timedelta(days=1) # 今日收入 - today_income = DailyIncomeStat.objects.filter(date=today).first() + today_income = DailyIncomeStat.query.filter(date=today).first() today_income_amount = float(today_income.total_amount) if today_income else 0.00 today_income_count = today_income.total_count if today_income else 0 # 今日支出 - today_payout = DailyPayoutStat.objects.filter(date=today).first() + today_payout = DailyPayoutStat.query.filter(date=today).first() today_payout_amount = float(today_payout.total_amount) if today_payout else 0.00 today_payout_count = today_payout.total_count if today_payout else 0 # 今日订单(已付款及之后的状态) - today_orders = Dingdan.objects.filter( + today_orders = Dingdan.query.filter( create_time__gte=today_start, create_time__lt=today_end, zhuangtai__in=[1,2,3,4,5,6,7,8] @@ -6269,7 +6473,7 @@ class CaiwuView(APIView): today_order_amount = float(today_orders.aggregate(total=Sum('jine'))['total'] or 0.00) # 今日成交(zhuangtai=3) - today_completed = Dingdan.objects.filter( + today_completed = Dingdan.query.filter( create_time__gte=today_start, create_time__lt=today_end, zhuangtai=3 @@ -6278,7 +6482,7 @@ class CaiwuView(APIView): today_completed_amount = float(today_completed.aggregate(total=Sum('jine'))['total'] or 0.00) # 今日退款(zhuangtai=5) - today_tuikuan = Dingdan.objects.filter( + today_tuikuan = Dingdan.query.filter( create_time__gte=today_start, create_time__lt=today_end, zhuangtai=5 @@ -6287,7 +6491,7 @@ class CaiwuView(APIView): today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('jine'))['total'] or 0.00) # 今日充值(会员充值,leixing=1, zhuangtai=3) - today_chongzhi = Czjilu.objects.filter( + today_chongzhi = Czjilu.query.filter( create_time__gte=today_start, create_time__lt=today_end, leixing=1, @@ -6297,7 +6501,7 @@ class CaiwuView(APIView): today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) # 今日新增会员:从购买记录表统计今天创建的用户(去重) - new_huiyuan_user_ids = Huiyuangoumai.objects.filter( + new_huiyuan_user_ids = Huiyuangoumai.query.filter( create_time__gte=today_start, create_time__lt=today_end ).values_list('yonghu_id', flat=True).distinct() @@ -6306,7 +6510,7 @@ class CaiwuView(APIView): # 新增会员充值总额(这些用户今天的会员充值金额) today_new_huiyuan_amount = 0.0 if new_huiyuan_user_ids: - amount_sum = Czjilu.objects.filter( + amount_sum = Czjilu.query.filter( create_time__gte=today_start, create_time__lt=today_end, yonghuid__in=new_huiyuan_user_ids, @@ -6316,22 +6520,22 @@ class CaiwuView(APIView): today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) # 各角色可提现余额总和 - all_guanshi_yue = float(UserGuanshi.objects.aggregate(total=Sum('yue'))['total'] or 0.00) - all_dashou_yue = float(UserDashou.objects.aggregate(total=Sum('yue'))['total'] or 0.00) - all_zuzhang_yue = float(UserZuzhang.objects.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) - all_shangjia_yue = float(UserShangjia.objects.aggregate(total=Sum('yue'))['total'] or 0.00) + all_guanshi_yue = float(UserGuanshi.query.aggregate(total=Sum('yue'))['total'] or 0.00) + all_dashou_yue = float(UserDashou.query.aggregate(total=Sum('yue'))['total'] or 0.00) + all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) + all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00) # 平台总收支及利润 - total_income = float(DailyIncomeStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) - total_payout = float(DailyPayoutStat.objects.aggregate(total=Sum('total_amount'))['total'] or 0.00) + total_income = float(DailyIncomeStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) + total_payout = float(DailyPayoutStat.query.aggregate(total=Sum('total_amount'))['total'] or 0.00) platform_profit = round(total_income - total_payout, 2) # 近30天每日收支明细 daily_stats = [] - income_qs = DailyIncomeStat.objects.order_by('-date')[:30] + income_qs = DailyIncomeStat.query.order_by('-date')[:30] payout_dict = { str(p.date): float(p.total_amount) - for p in DailyPayoutStat.objects.filter(date__gte=today - timedelta(days=30)) + for p in DailyPayoutStat.query.filter(date__gte=today - timedelta(days=30)) } for inc in income_qs: d = str(inc.date) @@ -6395,8 +6599,8 @@ class CwhybkhqView(APIView): return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) bankuai_list = [] - for bk in Bankuai.objects.all(): - members = Huiyuan.objects.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') + for bk in Bankuai.query.all(): + members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id') bankuai_list.append({ 'bankuai_id': bk.bankuai_id, 'mingcheng': bk.mingcheng, @@ -6504,7 +6708,7 @@ class HybkjtsjView(APIView): } # 充值分组查询 - chongzhi_qs = Czjilu.objects.filter(**chongzhi_filter) \ + chongzhi_qs = Czjilu.query.filter(**chongzhi_filter) \ .annotate(period=trunc_func) \ .values('period') \ .annotate( @@ -6520,7 +6724,7 @@ class HybkjtsjView(APIView): fenhong_agg['guanshi_amount'] = Sum('fenhong') if include_zuzhang: fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong') - fenhong_qs = Gsfenhong.objects.filter(**fenhong_filter) \ + fenhong_qs = Gsfenhong.query.filter(**fenhong_filter) \ .annotate(period=trunc_func) \ .values('period') \ .annotate(**fenhong_agg) \ @@ -6568,7 +6772,7 @@ class HybkjtsjView(APIView): else: summary_chongzhi_filter['create_time__year'] = summary_q.year - summary_chongzhi = Czjilu.objects.filter(**summary_chongzhi_filter).aggregate( + summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate( total_count=Count('id'), total_amount=Sum('jine') ) @@ -6592,7 +6796,7 @@ class HybkjtsjView(APIView): fenhong_agg['guanshi_sum'] = Sum('fenhong') if include_zuzhang: fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong') - summary_fenhong = Gsfenhong.objects.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) + summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg) total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0) total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0) @@ -6693,13 +6897,13 @@ class SzxxView(APIView): return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400) # ---------- 收入分组查询 ---------- - income_qs = DailyIncomeStat.objects.filter(**income_filter) \ + income_qs = DailyIncomeStat.query.filter(**income_filter) \ .values(group_field) \ .annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \ .order_by(group_field) # ---------- 支出分组查询 ---------- - payout_qs = DailyPayoutStat.objects.filter(**payout_filter) \ + payout_qs = DailyPayoutStat.query.filter(**payout_filter) \ .values(group_field) \ .annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \ .order_by(group_field) @@ -6741,10 +6945,10 @@ class SzxxView(APIView): time_series.sort(key=lambda x: x['date']) # ---------- 汇总数据 ---------- - income_summary = DailyIncomeStat.objects.filter(**summary_filter).aggregate( + income_summary = DailyIncomeStat.query.filter(**summary_filter).aggregate( total_income=Sum('total_amount'), total_count=Sum('total_count') ) - payout_summary = DailyPayoutStat.objects.filter(**summary_filter).aggregate( + payout_summary = DailyPayoutStat.query.filter(**summary_filter).aggregate( total_payout=Sum('total_amount'), total_count=Sum('total_count') ) total_income = float(income_summary['total_income'] or 0) @@ -6796,7 +7000,7 @@ class CwddhqlxView(APIView): return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403) # 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称 - types = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values('id', 'jieshao') + types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao') return Response({ 'code': 0, 'msg': 'ok', @@ -6888,7 +7092,7 @@ class CwhqjtddsjView(APIView): filters['fadan_pingtai'] = order_source # ---------- 时间序列分组查询 ---------- - qs = Dingdan.objects.filter(**filters) \ + qs = Dingdan.query.filter(**filters) \ .annotate(period=trunc_func) \ .values('period') \ .annotate( @@ -6941,7 +7145,7 @@ class CwhqjtddsjView(APIView): }) # ---------- 汇总数据 ---------- - summary_qs = Dingdan.objects.filter(**filters) + summary_qs = Dingdan.query.filter(**filters) total_order_count = summary_qs.count() total_order_amount = summary_qs.aggregate(s=Sum('jine'))['s'] or 0 total_success_count = summary_qs.filter(zhuangtai=3).count() @@ -7074,7 +7278,7 @@ class CwqtczhqView(APIView): # 1. 处理充值记录(Czjilu)—— 完全不变 cz_types = [t for t in types if t in [2, 3, 4, 5]] if cz_types: - cz_qs = Czjilu.objects.filter( + cz_qs = Czjilu.query.filter( create_time__gte=start_dt, create_time__lt=end_dt, leixing__in=cz_types, @@ -7108,7 +7312,7 @@ class CwqtczhqView(APIView): # 2. 处理罚款(Fadan)—— 修改:直接从 Fadan 表获取分红总额和分红笔数 if 6 in types: # 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数 - penalty_qs = Fadan.objects.filter( + penalty_qs = Fadan.query.filter( create_time__gte=start_dt, create_time__lt=end_dt, zhuangtai=2 # 已缴纳 @@ -7162,7 +7366,7 @@ class CwqtczhqView(APIView): filter_kwargs['create_time__month'] = summary_q.month else: filter_kwargs['create_time__year'] = summary_q.year - agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) + agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) count = agg['count'] or 0 amount = float(agg['amount'] or 0) profit = amount if lx == 3 else 0.0 @@ -7191,7 +7395,7 @@ class CwqtczhqView(APIView): penalty_filter['create_time__year'] = summary_q.year # 一次聚合:罚款笔数、金额、分红总额、分红笔数 - agg = Fadan.objects.filter(**penalty_filter).aggregate( + agg = Fadan.query.filter(**penalty_filter).aggregate( count=Count('id'), amount=Sum('fakuanjine'), fenhong_amount=Sum('shenqingren_fenhong_jine'), @@ -7309,7 +7513,7 @@ class CwqtczhqView(APIView): # 1. 处理充值记录(Czjilu) cz_types = [t for t in types if t in [2, 3, 4, 5]] if cz_types: - cz_qs = Czjilu.objects.filter( + cz_qs = Czjilu.query.filter( create_time__gte=start_dt, create_time__lt=end_dt, leixing__in=cz_types, @@ -7343,7 +7547,7 @@ class CwqtczhqView(APIView): # 2. 处理罚款(Fadan)—— 包含分红 if 6 in types: # 罚款记录 - penalty_qs = Fadan.objects.filter( + penalty_qs = Fadan.query.filter( create_time__gte=start_dt, create_time__lt=end_dt, zhuangtai=2 # 已缴纳 @@ -7355,7 +7559,7 @@ class CwqtczhqView(APIView): ).order_by('period') # 分红记录 - fenhong_qs = FadanFenhong.objects.filter( + fenhong_qs = FadanFenhong.query.filter( create_time__gte=start_dt, create_time__lt=end_dt ).annotate( @@ -7419,7 +7623,7 @@ class CwqtczhqView(APIView): filter_kwargs['create_time__month'] = summary_q.month else: filter_kwargs['create_time__year'] = summary_q.year - agg = Czjilu.objects.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) + agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine')) count = agg['count'] or 0 amount = float(agg['amount'] or 0) profit = amount if lx == 3 else 0.0 @@ -7452,12 +7656,12 @@ class CwqtczhqView(APIView): penalty_filter['create_time__year'] = summary_q.year fenhong_filter['create_time__year'] = summary_q.year - penalty_agg = Fadan.objects.filter(**penalty_filter).aggregate( + penalty_agg = Fadan.query.filter(**penalty_filter).aggregate( count=Count('id'), amount=Sum('fakuanjine')) penalty_count = penalty_agg['count'] or 0 penalty_amount = float(penalty_agg['amount'] or 0) - fenhong_agg = FadanFenhong.objects.filter(**fenhong_filter).aggregate( + fenhong_agg = FadanFenhong.query.filter(**fenhong_filter).aggregate( fh_count=Count('id'), fh_amount=Sum('ketixian_fenhong')) fh_count = fenhong_agg['fh_count'] or 0 fh_amount = float(fenhong_agg['fh_amount'] or 0) @@ -7514,9 +7718,9 @@ class KhpzhqView(APIView): if 'kaohepeizhi' not in permissions: return Response({'code': 403, 'msg': '无考核配置权限'}, status=403) - bankuai_list = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng')) - chenghao_qs = Chenghao.objects.prefetch_related('kaohecishufeiyong_set').all() + chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all() chenghao_list = [] for ch in chenghao_qs: # 获取费用:按次数排序,转为 {cishu: feiyong} 字典 @@ -7590,11 +7794,11 @@ class ChzsgcView(APIView): return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.objects.filter(mingcheng=mingcheng).exists(): + if Chenghao.query.filter(mingcheng=mingcheng).exists(): return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) with transaction.atomic(): - ch = Chenghao.objects.create( + ch = Chenghao.query.create( mingcheng=mingcheng, leixing=leixing, bankuai_id=bankuai_id, @@ -7607,13 +7811,13 @@ class ChzsgcView(APIView): for item in feiyong_list: cishu = int(item.get('cishu', 1)) feiyong = float(item.get('feiyong', 0)) - KaoheCishuFeiyong.objects.update_or_create( + KaoheCishuFeiyong.query.update_or_create( chenghao=ch, cishu=cishu, defaults={'feiyong': feiyong} ) else: # 默认添加第一次免费 - KaoheCishuFeiyong.objects.create(chenghao=ch, cishu=1, feiyong=0) + KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0) return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}}) @@ -7623,7 +7827,7 @@ class ChzsgcView(APIView): if not ch_id: return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) try: - ch = Chenghao.objects.get(id=ch_id) + ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) @@ -7638,7 +7842,7 @@ class ChzsgcView(APIView): return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400) if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']: return Response({'code': 1, 'msg': '无效的称号类型'}, status=400) - if Chenghao.objects.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): + if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists(): return Response({'code': 1, 'msg': '称号名称已存在'}, status=400) ch.mingcheng = mingcheng @@ -7655,7 +7859,7 @@ class ChzsgcView(APIView): with transaction.atomic(): ch.kaohecishufeiyong_set.all().delete() for item in feiyong_list: - KaoheCishuFeiyong.objects.create( + KaoheCishuFeiyong.query.create( chenghao=ch, cishu=int(item.get('cishu', 1)), feiyong=float(item.get('feiyong', 0)) @@ -7669,7 +7873,7 @@ class ChzsgcView(APIView): if not ch_id: return Response({'code': 1, 'msg': '缺少称号ID'}, status=400) try: - ch = Chenghao.objects.get(id=ch_id) + ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) ch.delete() @@ -7683,12 +7887,12 @@ class ChzsgcView(APIView): if not ch_id or not cishu: return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400) try: - ch = Chenghao.objects.get(id=ch_id) + ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) if int(cishu) < 1: return Response({'code': 1, 'msg': '次数必须>=1'}, status=400) - KaoheCishuFeiyong.objects.update_or_create( + KaoheCishuFeiyong.query.update_or_create( chenghao=ch, cishu=cishu, defaults={'feiyong': feiyong} ) @@ -7703,10 +7907,10 @@ class ChzsgcView(APIView): if int(cishu) == 1: return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400) try: - ch = Chenghao.objects.get(id=ch_id) + ch = Chenghao.query.get(id=ch_id) except Chenghao.DoesNotExist: return Response({'code': 1, 'msg': '称号不存在'}, status=404) - KaoheCishuFeiyong.objects.filter(chenghao=ch, cishu=cishu).delete() + KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete() return Response({'code': 0, 'msg': '删除成功'}) # ========== 修改某次费用(等价于add) ========== @@ -7768,7 +7972,7 @@ class KhgglView(APIView): filter_bankuai_name = request.data.get('bankuai_name', '').strip() # 基础查询:所有审核官,预加载用户及打手扩展表 - queryset = UserShenheguan.objects.select_related('user__dashou_profile') + queryset = UserShenheguan.query.select_related('user__dashou_profile') if filter_yonghuid: queryset = queryset.filter(user__yonghuid=filter_yonghuid) @@ -7779,10 +7983,10 @@ class KhgglView(APIView): # 如果填写了板块名称,筛选出关联该板块的审核官 if filter_bankuai_name: - bankuai_ids = Bankuai.objects.filter( + bankuai_ids = Bankuai.query.filter( mingcheng__icontains=filter_bankuai_name ).values_list('bankuai_id', flat=True) - shenheguan_ids = KaoheguanBankuai.objects.filter( + shenheguan_ids = KaoheguanBankuai.query.filter( bankuai_id__in=bankuai_ids ).values_list('kaoheguan_id', flat=True) queryset = queryset.filter(user__yonghuid__in=shenheguan_ids) @@ -7794,10 +7998,10 @@ class KhgglView(APIView): # ---------- 预计算每个板块的打手数量 ---------- bankuai_dashou_count_map = {} - all_banquai = Bankuai.objects.all() + all_banquai = Bankuai.query.all() for bk in all_banquai: - tag_ids = Chenghao.objects.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) - count = YonghuChenghao.objects.filter( + tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True) + count = YonghuChenghao.query.filter( chenghao_id__in=tag_ids, yonghu__dashou_profile__isnull=False ).values('yonghu').distinct().count() @@ -7806,10 +8010,10 @@ class KhgglView(APIView): # ---------- 新增:板块标签统计 ---------- bankuai_tags_data = [] for bk in all_banquai: - tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') + tags = Chenghao.query.filter(bankuai=bk, leixing='dashou') tag_list = [] for tag in tags: - user_count = YonghuChenghao.objects.filter(chenghao=tag).values('yonghu').distinct().count() + user_count = YonghuChenghao.query.filter(chenghao=tag).values('yonghu').distinct().count() tag_list.append({ 'id': tag.id, 'name': tag.mingcheng, @@ -7826,7 +8030,7 @@ class KhgglView(APIView): for sg in shenheguan_list: user = sg.user # 获取该审核官关联的板块 - bk_qs = KaoheguanBankuai.objects.filter(kaoheguan_id=user.yonghuid).select_related('bankuai') + bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.yonghuid).select_related('bankuai') bankuai_info = [] for bk in bk_qs: bid = bk.bankuai.bankuai_id @@ -7855,7 +8059,7 @@ class KhgglView(APIView): 'is_renzheng': sg.is_renzheng, }) - all_bankuai = list(Bankuai.objects.values('bankuai_id', 'mingcheng')) + all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng')) return Response({ 'code': 0, @@ -7895,7 +8099,7 @@ class ShgxgsjView(APIView): return Response({'code': 1, 'msg': '缺少用户ID'}, status=400) try: - shenheguan = UserShenheguan.objects.select_related('user').get(user__yonghuid=yonghuid) + shenheguan = UserShenheguan.query.select_related('user').get(user__yonghuid=yonghuid) except UserShenheguan.DoesNotExist: return Response({'code': 1, 'msg': '审核官不存在'}, status=404) @@ -7924,18 +8128,18 @@ class ShgxgsjView(APIView): bankuai_id = request.data.get('bankuai_id') if not bankuai_id: return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - if not Bankuai.objects.filter(bankuai_id=bankuai_id).exists(): + if not Bankuai.query.filter(bankuai_id=bankuai_id).exists(): return Response({'code': 1, 'msg': '板块不存在'}, status=400) - if KaoheguanBankuai.objects.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): + if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists(): return Response({'code': 1, 'msg': '已关联该板块'}, status=400) - KaoheguanBankuai.objects.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) + KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id) return Response({'code': 0, 'msg': '板块关联成功'}) elif action == 'remove_bankuai': bankuai_id = request.data.get('bankuai_id') if not bankuai_id: return Response({'code': 1, 'msg': '缺少板块ID'}, status=400) - deleted, _ = KaoheguanBankuai.objects.filter( + deleted, _ = KaoheguanBankuai.query.filter( kaoheguan_id=yonghuid, bankuai_id=bankuai_id ).delete() if not deleted: @@ -7989,7 +8193,7 @@ class ZxkfghdsView(APIView): # ========== 3. 查询原订单及关联信息 ========== try: - old_order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + old_order = Dingdan.query.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: logger.warning(f"订单不存在: {dingdan_id}") return Response({'code': 404, 'msg': '订单不存在'}) @@ -8004,7 +8208,7 @@ class ZxkfghdsView(APIView): # 获取原链接(用于模板ID等) try: - old_lianjie = ShangjiaLianjie.objects.get(dingdan_id=dingdan_id) + old_lianjie = ShangjiaLianjie.query.get(dingdan_id=dingdan_id) except ShangjiaLianjie.DoesNotExist: logger.error(f"原链接记录不存在,订单: {dingdan_id}") return Response({'code': 500, 'msg': '链接数据异常'}) @@ -8014,7 +8218,7 @@ class ZxkfghdsView(APIView): shangjia_ext = old_order.shangjia_kuozhan shangjia_id = shangjia_ext.shangjia_id # 获取商家昵称用于新订单扩展表 - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_user = UserMain.query.get(yonghuid=shangjia_id) shangjia_nicheng = shangjia_user.shop_profile.nicheng or f"商家{shangjia_id}" except (ObjectDoesNotExist, UserShangjia.DoesNotExist): logger.error(f"商家信息缺失,订单: {dingdan_id}") @@ -8031,7 +8235,7 @@ class ZxkfghdsView(APIView): dashou_id = old_order.jiedan_dashou_id if dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 # 退款次数+1 dashou_profile.zhuangtai = 1 # 设为空闲 @@ -8040,7 +8244,7 @@ class ZxkfghdsView(APIView): logger.warning(f"打手 {dashou_id} 不存在,跳过更新") # 5.3 更新退款记录表 - Tuikuanjilu.objects.update_or_create( + Tuikuanjilu.query.update_or_create( dingdan_id=dingdan_id, defaults={ 'dashouid': dashou_id or '', @@ -8090,7 +8294,7 @@ class ZxkfghdsView(APIView): salt = secrets.token_hex(8) raw = f"{timestamp}:{dd_id}:{uid}:{salt}" token = hashlib.sha256(raw.encode()).hexdigest()[:24] - if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): + if ShangjiaLianjie.query.filter(lianjie_token=token).exists(): return generate_secure_token(dd_id, uid) return token @@ -8102,7 +8306,7 @@ class ZxkfghdsView(APIView): secure_token = generate_secure_token(new_dingdan_id, shangjia_id) # ---- 6.2 创建新订单(复制核心数据,状态直接为1) ---- - new_order = Dingdan.objects.create( + new_order = Dingdan.query.create( dingdan_id=new_dingdan_id, zhuangtai=1, # 下单中 fadan_pingtai=2, @@ -8121,7 +8325,7 @@ class ZxkfghdsView(APIView): ) # ---- 6.3 创建商家扩展表 ---- - DingdanShangjia.objects.create( + DingdanShangjia.query.create( dingdan=new_order, shangjia_id=shangjia_id, sjnicheng=shangjia_nicheng, @@ -8129,7 +8333,7 @@ class ZxkfghdsView(APIView): # ---- 6.4 生成新链接并关联旧链接信息 ---- link_url = generate_link_url(secure_token) - new_lianjie = ShangjiaLianjie.objects.create( + new_lianjie = ShangjiaLianjie.query.create( yonghu_id=shangjia_id, leixing_id=old_lianjie.leixing_id, moban_id=old_lianjie.moban_id, @@ -8174,7 +8378,7 @@ class ZxkfghdsView(APIView): def _get_game_type_name(self, leixing_id): """根据类型ID获取游戏类型名称""" try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception: return '游戏订单' diff --git a/config/models.py b/config/models.py index fa13249..7bf259f 100644 --- a/config/models.py +++ b/config/models.py @@ -1,7 +1,8 @@ from django.db import models +from gvsdsdk.model_base import QModel -class Lunbo(models.Model): +class Lunbo(QModel): tupian_url = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL') leixing = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1:轮播2:个人中心背景') @@ -14,7 +15,7 @@ class Lunbo(models.Model): # 不要索引!数据量太小 -class Gonggao(models.Model): +class Gonggao(QModel): leixing = models.IntegerField(null=True, blank=True, verbose_name='公告类型') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') @@ -25,7 +26,7 @@ class Gonggao(models.Model): verbose_name_plural = '公告' # 不要索引!数据量太小 -class Qunpeizhi(models.Model): +class Qunpeizhi(QModel): peizhiid = models.IntegerField(null=True, blank=True, verbose_name='群类型:1.打手 2.管事') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更改时间') @@ -35,7 +36,7 @@ class Qunpeizhi(models.Model): class Meta: db_table = 'qunpeizhi' -class Tupianpeizhi(models.Model): +class Tupianpeizhi(QModel): tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL') leixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2:默认头像图片') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') @@ -47,7 +48,7 @@ class Tupianpeizhi(models.Model): verbose_name_plural = '配置图片' -class Szjilu(models.Model): +class Szjilu(QModel): id = models.AutoField(primary_key=True, verbose_name='记录ID') zongsy = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益') zongls = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水') @@ -68,7 +69,7 @@ class Szjilu(models.Model): -class ShangjiaMoban(models.Model): +class ShangjiaMoban(QModel): """商家订单模板表 - 存储商家创建的订单模板""" # 主键ID(自动递增) @@ -119,7 +120,7 @@ class ShangjiaMoban(models.Model): -class ShangjiaLianjie(models.Model): +class ShangjiaLianjie(QModel): """商家链接存储表 - 存储生成的派单链接""" # 主键ID(自动递增) @@ -208,7 +209,7 @@ class ShangjiaLianjie(models.Model): -class TixianQuotaDefault(models.Model): +class TixianQuotaDefault(QModel): """ 默认每日提现限额表 """ @@ -239,7 +240,7 @@ class TixianQuotaDefault(models.Model): -class WithdrawConfig(models.Model): +class WithdrawConfig(QModel): """ 提现模式配置表 - mode = 1: 自动提现(前端展示收款码模式) @@ -270,7 +271,7 @@ class WithdrawConfig(models.Model): -class AccountPermission(models.Model): +class AccountPermission(QModel): """账号权限表:记录每个账号拥有的权限及开关状态""" account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') permission_code = models.IntegerField(verbose_name='权限代码') @@ -292,7 +293,7 @@ class AccountPermission(models.Model): # apps/dingdan/models.py (新增模型) -class DailyIncomeStat(models.Model): +class DailyIncomeStat(QModel): """ 每日收入统计表 记录平台每天通过微信支付获得的总收入和总笔数 @@ -326,7 +327,7 @@ class DailyIncomeStat(models.Model): # apps/dingdan/models.py -class DailyPayoutStat(models.Model): +class DailyPayoutStat(QModel): """ 每日出款统计表 记录平台每天通过提现/结算等方式的出款总额和总笔数 @@ -359,7 +360,7 @@ class DailyPayoutStat(models.Model): -class PopupPage(models.Model): +class PopupPage(QModel): page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识') name = models.CharField(max_length=100, verbose_name='页面名称') description = models.TextField(blank=True, verbose_name='描述') @@ -377,7 +378,7 @@ class PopupPage(models.Model): return f'{self.name} ({self.page_key})' -class PopupConfig(models.Model): +class PopupConfig(QModel): STRATEGY_CHOICES = ( ('once', '仅一次'), ('daily', '每日'), @@ -416,7 +417,7 @@ class PopupConfig(models.Model): return f'{self.page.name} - {self.popup_id}' -class PopupImage(models.Model): +class PopupImage(QModel): popup_config = models.ForeignKey(PopupConfig, on_delete=models.CASCADE, related_name='images') image_url = models.CharField(max_length=500, verbose_name='图片相对路径') text = models.TextField(blank=True, verbose_name='图片上方文字') diff --git a/config/views.py b/config/views.py index 30e4ef6..80c30cf 100644 --- a/config/views.py +++ b/config/views.py @@ -11,7 +11,8 @@ from decimal import Decimal, InvalidOperation from django.conf import settings from django.db import transaction, connection -from django.db.models import Q, F, Max, Prefetch +from django.db.models import Q, F, Max, Prefetch +from gvsdsdk.fluent import db, func, FQ from django.core.cache import cache from django.core.paginator import Paginator from django.utils import timezone @@ -130,11 +131,11 @@ class ShangpinGonggaoView(APIView): try: logger.info("收到商品公告和轮播图请求") # 1. 查询公告(类型为1) - gonggao_obj = Gonggao.objects.filter(leixing=1).first() + gonggao_obj = Gonggao.query.filter(leixing=1).first() # 获取公告内容,如果没有则返回空字符串 gonggao_content = gonggao_obj.jieshao if gonggao_obj else "" # 2. 查询轮播图(类型为1,按显示顺序排序) - lunbo_queryset = Lunbo.objects.filter(leixing=1) + lunbo_queryset = Lunbo.query.filter(leixing=1) # 提取轮播图URL列表 lunbo_urls = [lunbo.tupian_url for lunbo in lunbo_queryset] # 3. 构建返回数据 @@ -209,11 +210,11 @@ class AdminConfigQueryView(APIView): }, status=status.HTTP_401_UNAUTHORIZED) # 查询图片配置 - rule_image = Tupianpeizhi.objects.filter(leixing=1).first() - default_avatar = Tupianpeizhi.objects.filter(leixing=2).first() + rule_image = Tupianpeizhi.query.filter(leixing=1).first() + default_avatar = Tupianpeizhi.query.filter(leixing=2).first() # 查询群配置 - qun_configs = Qunpeizhi.objects.all() + qun_configs = Qunpeizhi.query.all() qq_groups = [] for config in qun_configs: qq_groups.append({ @@ -224,12 +225,12 @@ class AdminConfigQueryView(APIView): }) # 查询利率配置 - platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() - shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() + platform_rate_obj = Lilubiao.query.filter(fadanpingtai='1').first() + shop_rate_obj = Lilubiao.query.filter(fadanpingtai='3').first() # 🔴【新增】打手提现抽成利率(字符5) - dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + dashou_withdraw_obj = Lilubiao.query.filter(fadanpingtai='5').first() # 🔴【新增】管事提现抽成利率(字符6) - guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + guanshi_withdraw_obj = Lilubiao.query.filter(fadanpingtai='6').first() # 构建返回数据 data = { @@ -372,7 +373,7 @@ class AdminUploadImageView(APIView): # 更新数据库 with transaction.atomic(): # 获取或创建图片配置记录 - tupian_peizhi, created = Tupianpeizhi.objects.get_or_create(leixing=int(leixing)) + tupian_peizhi, created = Tupianpeizhi.query.get_or_create(leixing=int(leixing)) tupian_peizhi.tupian = fixed_relative_url tupian_peizhi.save() @@ -452,7 +453,7 @@ class AdminUpdateConfigView(APIView): continue # 🔴 修复:使用filter()确保查询到正确的记录 - qun_configs = Qunpeizhi.objects.filter(peizhiid=peizhiid) + qun_configs = Qunpeizhi.query.filter(peizhiid=peizhiid) if qun_configs.exists(): # 如果存在多个相同peizhiid的记录,取第一个(应该只有一个) @@ -473,7 +474,7 @@ class AdminUpdateConfigView(APIView): qun_config.save() else: # 如果不存在,创建新的(使用提供的peizhiid) - qun_config = Qunpeizhi.objects.create( + qun_config = Qunpeizhi.query.create( peizhiid=peizhiid, neirong=group_data.get('neirong', '') or '', qunid=group_data.get('qunid', '') or '', @@ -502,7 +503,7 @@ class AdminUpdateConfigView(APIView): platform_rate_float = float(platform_rate) if 0 <= platform_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 - platform_rate_obj = Lilubiao.objects.filter(fadanpingtai='1').first() + platform_rate_obj = Lilubiao.query.filter(fadanpingtai='1').first() if platform_rate_obj: # 更新现有记录 @@ -510,7 +511,7 @@ class AdminUpdateConfigView(APIView): platform_rate_obj.save() else: # 创建新记录 - platform_rate_obj = Lilubiao.objects.create( + platform_rate_obj = Lilubiao.query.create( lilu=platform_rate_float, fadanpingtai='1' ) @@ -536,7 +537,7 @@ class AdminUpdateConfigView(APIView): shop_rate_float = float(shop_rate) if 0 <= shop_rate_float <= 1: # 🔴 修复:查询现有记录,如果不存在则创建 - shop_rate_obj = Lilubiao.objects.filter(fadanpingtai='3').first() + shop_rate_obj = Lilubiao.query.filter(fadanpingtai='3').first() if shop_rate_obj: # 更新现有记录 @@ -544,7 +545,7 @@ class AdminUpdateConfigView(APIView): shop_rate_obj.save() else: # 创建新记录 - shop_rate_obj = Lilubiao.objects.create( + shop_rate_obj = Lilubiao.query.create( lilu=shop_rate_float, fadanpingtai='3' ) @@ -573,7 +574,7 @@ class AdminUpdateConfigView(APIView): dashou_withdraw_rate_float = float(dashou_withdraw_rate) if 0 <= dashou_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 - dashou_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + dashou_withdraw_obj = Lilubiao.query.filter(fadanpingtai='5').first() if dashou_withdraw_obj: # 更新现有记录 @@ -581,7 +582,7 @@ class AdminUpdateConfigView(APIView): dashou_withdraw_obj.save() else: # 创建新记录 - dashou_withdraw_obj = Lilubiao.objects.create( + dashou_withdraw_obj = Lilubiao.query.create( lilu=dashou_withdraw_rate_float, fadanpingtai='5' ) @@ -607,7 +608,7 @@ class AdminUpdateConfigView(APIView): guanshi_withdraw_rate_float = float(guanshi_withdraw_rate) if 0 <= guanshi_withdraw_rate_float <= 1: # 查询现有记录,如果不存在则创建 - guanshi_withdraw_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + guanshi_withdraw_obj = Lilubiao.query.filter(fadanpingtai='6').first() if guanshi_withdraw_obj: # 更新现有记录 @@ -615,7 +616,7 @@ class AdminUpdateConfigView(APIView): guanshi_withdraw_obj.save() else: # 创建新记录 - guanshi_withdraw_obj = Lilubiao.objects.create( + guanshi_withdraw_obj = Lilubiao.query.create( lilu=guanshi_withdraw_rate_float, fadanpingtai='6' ) @@ -786,7 +787,7 @@ class DashouPaihangView(APIView): chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1) # 查询打手扩展表,并关联用户主表 - dashou_list = UserDashou.objects.select_related('user').filter( + dashou_list = UserDashou.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] @@ -852,7 +853,7 @@ class DashouPaihangView(APIView): chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询管事扩展表,并关联用户主表 - guanshi_list = UserGuanshi.objects.select_related('user').filter( + guanshi_list = UserGuanshi.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] @@ -878,7 +879,7 @@ class DashouPaihangView(APIView): user_main = guanshi.user # 管事的昵称需要查询老板扩展表 - boss_profile = UserBoss.objects.filter(user=user_main).first() + boss_profile = UserBoss.query.filter(user=user_main).first() guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' # 获取头像(相对URL) @@ -919,7 +920,7 @@ class DashouPaihangView(APIView): chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1) # 查询商家扩展表,并关联用户主表 - shangjia_list = UserShangjia.objects.select_related('user').filter( + shangjia_list = UserShangjia.query.select_related('user').filter( chaxun_tiaojian ).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG] @@ -945,7 +946,7 @@ class DashouPaihangView(APIView): user_main = shangjia.user # 商家的昵称需要查询老板扩展表 - boss_profile = UserBoss.objects.filter(user=user_main).first() + boss_profile = UserBoss.query.filter(user=user_main).first() shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称' # 获取头像(相对URL) @@ -1365,14 +1366,14 @@ class ShangjiaMobanListView(APIView): if label_id: query &= Q(chenghao_id=label_id) - mobans = ShangjiaMoban.objects.filter(query).order_by('-id') + mobans = ShangjiaMoban.query.filter(query).order_by('-id') formatted = [] for moban in mobans: texiao_json = '' label_name = '' if moban.chenghao_id: try: - ch = Chenghao.objects.get(id=moban.chenghao_id) + ch = Chenghao.query.get(id=moban.chenghao_id) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: @@ -1393,7 +1394,7 @@ class ShangjiaMobanListView(APIView): return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}}) # 分页模式 - queryset = ShangjiaMoban.objects.filter(query).order_by('-id') + queryset = ShangjiaMoban.query.filter(query).order_by('-id') paginator = Paginator(queryset, page_size) if page < 1 or page > paginator.num_pages: return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) @@ -1405,7 +1406,7 @@ class ShangjiaMobanListView(APIView): label_name = '' if moban.chenghao_id: try: - ch = Chenghao.objects.get(id=moban.chenghao_id) + ch = Chenghao.query.get(id=moban.chenghao_id) label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or '' except Chenghao.DoesNotExist: @@ -1521,7 +1522,7 @@ class ShangjiaTianjiaMobanView(APIView): # 8. 验证商品类型是否存在且可用 try: - shangpin_obj = ShangpinLeixing.objects.get( + shangpin_obj = ShangpinLeixing.query.get( id=leixing_id, shenhezhuangtai=1 # 审核通过 ) @@ -1529,7 +1530,7 @@ class ShangjiaTianjiaMobanView(APIView): return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404) # 9. 检查同一用户、同一类型下是否已存在相同介绍的模板 - if ShangjiaMoban.objects.filter( + if ShangjiaMoban.query.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao @@ -1538,7 +1539,7 @@ class ShangjiaTianjiaMobanView(APIView): # 10. 执行数据库创建(事务) with transaction.atomic(): - moban = ShangjiaMoban.objects.create( + moban = ShangjiaMoban.query.create( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=jieshao, @@ -1597,7 +1598,7 @@ class ShangjiaShanchuMobanView(APIView): return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400) try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) + moban = ShangjiaMoban.query.get(id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404) @@ -1641,7 +1642,7 @@ class ShangjiaGengxinMobanView(APIView): # 查询模板 try: - moban = ShangjiaMoban.objects.get( + moban = ShangjiaMoban.query.get( id=moban_id, yonghu_id=yonghu_id, leixing_id=leixing_id @@ -1705,7 +1706,7 @@ class ShangjiaGengxinMobanView(APIView): # 检查介绍唯一性 if 'moban_jieshao' in update_fields: - if ShangjiaMoban.objects.filter( + if ShangjiaMoban.query.filter( yonghu_id=yonghu_id, leixing_id=leixing_id, moban_jieshao=moban.moban_jieshao @@ -1770,13 +1771,13 @@ class ShangjiaGenerateLinkView(APIView): # 商品类型 try: - shangpin_leixing = ShangpinLeixing.objects.get(id=shangpin_type_id, shenhezhuangtai=1) + shangpin_leixing = ShangpinLeixing.query.get(id=shangpin_type_id, shenhezhuangtai=1) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=400) # 查询模板 try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id) + moban = ShangjiaMoban.query.get(id=moban_id, yonghu_id=user.yonghuid, leixing_id=shangpin_type_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 400, 'msg': '模板不存在', 'data': {}}, status=400) @@ -1786,7 +1787,7 @@ class ShangjiaGenerateLinkView(APIView): # 利率 try: - lilu = float(Lilubiao.objects.get(fadanpingtai='3').lilu) or 1.0 + lilu = float(Lilubiao.query.get(fadanpingtai='3').lilu) or 1.0 except Lilubiao.DoesNotExist: lilu = 1.0 dashou_fencheng = Decimal(str(round(float(jiage) * lilu, 2))) @@ -1806,7 +1807,7 @@ class ShangjiaGenerateLinkView(APIView): label_name = '' if chenghao_id: try: - label_name = Chenghao.objects.get(id=chenghao_id).mingcheng + label_name = Chenghao.query.get(id=chenghao_id).mingcheng except Chenghao.DoesNotExist: pass @@ -1823,7 +1824,7 @@ class ShangjiaGenerateLinkView(APIView): raw = f"{timestamp}:{dd_id}:{uid}:{salt}" token = hashlib.sha256(raw.encode()).hexdigest()[:24] # 确保唯一性(极小概率重复再试一次) - if ShangjiaLianjie.objects.filter(lianjie_token=token).exists(): + if ShangjiaLianjie.query.filter(lianjie_token=token).exists(): return generate_secure_token(dd_id, uid) return token @@ -1836,7 +1837,7 @@ class ShangjiaGenerateLinkView(APIView): secure_token = generate_secure_token(dingdan_id, user.yonghuid) # 创建订单主表 - dingdan = Dingdan.objects.create( + dingdan = Dingdan.query.create( dingdan_id=dingdan_id, zhuangtai=13, fadan_pingtai=2, @@ -1852,7 +1853,7 @@ class ShangjiaGenerateLinkView(APIView): ) # 商家扩展表 - DingdanShangjia.objects.create( + DingdanShangjia.query.create( dingdan=dingdan, shangjia_id=user.yonghuid, sjnicheng=shangjia.nicheng or f"商家{user.yonghuid}", @@ -1861,13 +1862,13 @@ class ShangjiaGenerateLinkView(APIView): # 关联标签 if chenghao_id: try: - ch = Chenghao.objects.get(id=chenghao_id) - DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=ch) + ch = Chenghao.query.get(id=chenghao_id) + DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch) except Chenghao.DoesNotExist: logger.warning(f"称号{chenghao_id}不存在,跳过关联") # 更新商家余额和统计 - UserShangjia.objects.filter(id=shangjia.id).update( + UserShangjia.query.filter(id=shangjia.id).update( fabu=F('fabu') + 1, yue=F('yue') - jiage, jinridingdan=F('jinridingdan') + 1, @@ -1878,7 +1879,7 @@ class ShangjiaGenerateLinkView(APIView): # 生成链接并保存 link_url = generate_link_url(secure_token) - lianjie_obj = ShangjiaLianjie.objects.create( + lianjie_obj = ShangjiaLianjie.query.create( yonghu_id=user.yonghuid, leixing_id=shangpin_type_id, moban_id=moban_id, @@ -1890,7 +1891,7 @@ class ShangjiaGenerateLinkView(APIView): ) # 更新模板发布数量 - ShangjiaMoban.objects.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1) + ShangjiaMoban.query.filter(id=moban_id).update(fabu_shuliang=F('fabu_shuliang') + 1) shangjia.refresh_from_db() logger.info(f"链接生成成功 - 订单ID: {dingdan_id}, 标签: {label_name}") @@ -1937,11 +1938,11 @@ class ShangjiaLianjieListView(APIView): # 校验模板归属 try: - moban = ShangjiaMoban.objects.get(id=moban_id, yonghu_id=yonghu_id) + moban = ShangjiaMoban.query.get(id=moban_id, yonghu_id=yonghu_id) except ShangjiaMoban.DoesNotExist: return Response({'code': 404, 'msg': '模板不存在或无权查看', 'data': {}}, status=404) - links = ShangjiaLianjie.objects.filter( + links = ShangjiaLianjie.query.filter( yonghu_id=yonghu_id, moban_id=moban_id, is_used=(used == 1) @@ -2136,7 +2137,7 @@ class KehuGetDingdanLianjieView(APIView): # 5. 查询链接记录 try: - lianjie_obj = ShangjiaLianjie.objects.select_related().get(lianjie_token=token) + lianjie_obj = ShangjiaLianjie.query.select_related().get(lianjie_token=token) logger.info(f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}") except ShangjiaLianjie.DoesNotExist: logger.warning(f"链接不存在 - Token: {token[:8]}..., IP: {client_ip}") @@ -2184,12 +2185,12 @@ class KehuGetDingdanLianjieView(APIView): if lianjie_obj.dingdan_id: try: # 查询订单主表 - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=lianjie_obj.dingdan_id) # 获取商家昵称 shangjia_nicheng = '' try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) + user_main = UserMain.query.get(yonghuid=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_nicheng = shangjia_obj.nicheng except (UserMain.DoesNotExist, UserShangjia.DoesNotExist): @@ -2234,7 +2235,7 @@ class KehuGetDingdanLianjieView(APIView): # 9.2 获取商家标识 shangjia_biaoshi = '' try: - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').filter( + shangjia_kuozhan = DingdanShangjia.query.select_related('dingdan').filter( dingdan__dingdan_id=lianjie_obj.dingdan_id ).first() @@ -2435,7 +2436,7 @@ class KehuTianxieDingdanView(APIView): # 9. 查询链接记录 try: - lianjie_obj = ShangjiaLianjie.objects.get(lianjie_token=token) + lianjie_obj = ShangjiaLianjie.query.get(lianjie_token=token) logger.info( f"找到链接记录 - 链接ID: {lianjie_obj.id}, 商家ID: {lianjie_obj.yonghu_id}, 是否已使用: {lianjie_obj.is_used}") except ShangjiaLianjie.DoesNotExist: @@ -2471,7 +2472,7 @@ class KehuTianxieDingdanView(APIView): # 验证打手是否存在且有效 try: # 先查询用户主表 - dashou_user = UserMain.objects.get(yonghuid=zhiding_dashou) + dashou_user = UserMain.query.get(yonghuid=zhiding_dashou) logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") # 通过反向关系获取打手扩展信息 @@ -2531,7 +2532,7 @@ class KehuTianxieDingdanView(APIView): logger.error(f"链接记录中订单ID为空 - 链接ID: {lianjie_obj.id}") raise ValueError("订单ID为空") - dingdan_obj = Dingdan.objects.get(dingdan_id=lianjie_obj.dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=lianjie_obj.dingdan_id) # 检查订单状态是否为13(灰色状态) if dingdan_obj.zhuangtai != 13: @@ -2569,7 +2570,7 @@ class KehuTianxieDingdanView(APIView): # 获取商家信息用于返回 try: - user_main = UserMain.objects.get(yonghuid=lianjie_obj.yonghu_id) + user_main = UserMain.query.get(yonghuid=lianjie_obj.yonghu_id) shangjia_obj = user_main.shop_profile shangjia_mingcheng = shangjia_obj.nicheng except: @@ -2621,7 +2622,7 @@ class KehuTianxieDingdanView(APIView): def _get_game_type_name(self, leixing_id): try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: logger.error(f"获取游戏类型名称异常: {e}") @@ -2635,7 +2636,7 @@ class KehuTianxieDingdanView(APIView): try: # 获取游戏类型名称 try: - leixing_obj = ShangpinLeixing.objects.get(id=dingdan.leixing_id) + leixing_obj = ShangpinLeixing.query.get(id=dingdan.leixing_id) game_type = leixing_obj.jieshao except: game_type = '未知游戏' @@ -2697,7 +2698,7 @@ class GuanZhuAListView(APIView): def post(self, request): try: # 查询类型为3的所有记录 - qs = Tupianpeizhi.objects.filter(leixing=3) + qs = Tupianpeizhi.query.filter(leixing=3) if not qs.exists(): return Response({ 'code': 0, @@ -3168,7 +3169,7 @@ class PopupConfigView(APIView): # 3. 查询页面配置 try: - popup_page = PopupPage.objects.get(page_key=page_key, is_active=True) + popup_page = PopupPage.query.get(page_key=page_key, is_active=True) except PopupPage.DoesNotExist: # 该页面没有配置弹窗,返回空 return Response({ @@ -3181,7 +3182,7 @@ class PopupConfigView(APIView): # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) now = timezone.now() - popups = PopupConfig.objects.filter( + popups = PopupConfig.query.filter( page=popup_page, is_active=True, start_time__lte=now, @@ -3215,7 +3216,7 @@ class GetWithdrawModeView(APIView): def post(self, request, *args, **kwargs): # 尝试获取 ID=1 的记录 - config = WithdrawConfig.objects.filter(id=1).first() + config = WithdrawConfig.query.filter(id=1).first() # 如果存在则取实际值,否则默认 2(手动提现) mode = config.mode if config else 2 @@ -3278,16 +3279,16 @@ class CheckPhoneAuthView(APIView): """ try: # 先确认用户是打手 - UserDashou.objects.get(user=user) + UserDashou.query.get(user=user) except UserDashou.DoesNotExist: return False # 检查是否有任意会员购买记录 - return Huiyuangoumai.objects.filter(yonghu_id=user.yonghuid).exists() + return Huiyuangoumai.query.filter(yonghu_id=user.yonghuid).exists() def _shangjia_meets_threshold(self, user): """商家条件:发布订单总数 fabu >= 5""" try: - shop = UserShangjia.objects.get(user=user) + shop = UserShangjia.query.get(user=user) except UserShangjia.DoesNotExist: return False return shop.fabu >= 5 @@ -3295,7 +3296,7 @@ class CheckPhoneAuthView(APIView): def _guanshi_meets_threshold(self, user): """管事条件:充值分佣总额 chongzhifenrun >= 15""" try: - guanshi = UserGuanshi.objects.get(user=user) + guanshi = UserGuanshi.query.get(user=user) except UserGuanshi.DoesNotExist: return False return guanshi.chongzhifenrun >= 15 @@ -3303,7 +3304,7 @@ class CheckPhoneAuthView(APIView): def _zuzhang_meets_threshold(self, user): """组长条件:分佣总额 fenyong_zonge >= 2""" try: - zuzhang = UserZuzhang.objects.get(user=user) + zuzhang = UserZuzhang.query.get(user=user) except UserZuzhang.DoesNotExist: return False return zuzhang.fenyong_zonge >= 2 @@ -3311,7 +3312,7 @@ class CheckPhoneAuthView(APIView): def _boss_meets_threshold(self, user): """老板条件:消费总额 zonge >= 20""" try: - boss = UserBoss.objects.get(user=user) + boss = UserBoss.query.get(user=user) except UserBoss.DoesNotExist: return False return boss.zonge >= 20 \ No newline at end of file diff --git a/gvsdsdk/API.md b/gvsdsdk/API.md new file mode 100644 index 0000000..b68fcf6 --- /dev/null +++ b/gvsdsdk/API.md @@ -0,0 +1,666 @@ +# GVS-DSDK API 使用手册 + +> 版本:1.0.0 | 更新日期:2026-06-13 + +## 目录 + +- [1. 概述](#1-概述) +- [2. 快速开始](#2-快速开始) +- [3. 包结构](#3-包结构) +- [4. 用户管理 (UserManager)](#4-用户管理-usermanager) +- [5. 角色管理 (RoleManager)](#5-角色管理-rolemanager) +- [6. 商铺管理 (ShopManager)](#6-商铺管理-shopmanager) +- [7. 职务管理 (PositionManager)](#7-职务管理-positionmanager) +- [8. 权限验证 (PermissionVerifier)](#8-权限验证-permissionverifier) +- [9. ELT 认证](#9-elt-认证) +- [10. 分佣引擎 (CommissionEngine)](#10-分佣引擎-commissionengine) +- [11. 支付网关 (PaymentManager)](#11-支付网关-paymentmanager) +- [12. MSYNC 服务间通信](#12-msync-服务间通信) +- [13. FluentQuery ORM 查询](#13-fluentquery-orm-查询) +- [14. 数据库路由与建表](#14-数据库路由与建表) +- [15. 数据模型一览](#15-数据模型一览) + +--- + +## 1. 概述 + +GVS-DSDK 是 GVS 分布式服务 SDK,提供: + +- **SAAS 用户管理**:用户、角色、权限、商铺、职务的完整 CRUD 和关联管理 +- **ELT 认证**:Cookie → EphemeralToken → User 的无状态认证体系 +- **分佣引擎**:5 种分账策略 + 预结算 + 邀请链分佣 +- **支付网关**:微信/支付宝/银行卡三渠道适配 + 统一编排 +- **MSYNC 通信**:服务注册/发现/OGM/分布式事务 +- **FluentQuery**:SQLAlchemy 风格的 ORM 查询层 + +全程 PascalCase 命名,从数据库到前端无中间转换层。 + +--- + +## 2. 快速开始 + +### 安装 + +```bash +pip install -e /path/to/gvsdsdk +``` + +### settings.py 配置 + +```python +INSTALLED_APPS = [ + ... + 'gvsdsdk', +] + +# 方式一:子服务器连接 SAAS 主库(只读) +DATABASES = { + 'default': { ... }, # 本地数据库 + 'saas': { # SAAS 主库 + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'Users', + 'USER': 'root', + 'PASSWORD': '***', + 'HOST': 'saas-db.gvsds.com', + 'PORT': '3306', + }, +} +DATABASE_ROUTERS = ['gvsdsdk.db_router.SAASDatabaseRouter'] +GVSDSDK_SAAS_READ_ONLY = True # 默认 True + +# 方式二:同源数据库(可读写) +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'mydb', + ... + }, +} +DATABASE_ROUTERS = [] +GVSDSDK_MANAGE_TABLES = True # 允许 Django 管理表 +``` + +### 建表 + +```bash +# 方式一:Django migrate(需 GVSDSDK_MANAGE_TABLES=True) +python manage.py makemigrations gvsdsdk +python manage.py migrate + +# 方式二:gvsdsdk_init 命令(无需 GVSDSDK_MANAGE_TABLES) +python manage.py gvsdsdk_init --db default +python manage.py gvsdsdk_init --db default --dry-run # 仅打印 SQL +python manage.py gvsdsdk_init --db default --modules auth,commission,payment +``` + +--- + +## 3. 包结构 + +``` +gvsdsdk/ +├── __init__.py # 统一入口(惰性加载 90+ 符号) +├── models.py # SAAS 核心数据模型(17 张表) +├── model_base.py # QModel 元类 + FluentQuery 注入 +├── model_utils.py # UUIDObj +├── fluent.py # FluentQuery ORM 查询层 +├── db_router.py # SAASDatabaseRouter +├── apps.py # Django AppConfig +├── management/ +│ └── commands/ +│ └── gvsdsdk_init.py # 建表命令 +│ +├── auth/ # ELT 用户认证模块 +│ ├── elt_auth.py # ELT 认证后端 +│ ├── cookie.py # Cookie 管理 +│ ├── crypto.py # 密码哈希 + AES-256-GCM +│ ├── middleware.py # ELTAuthMiddleware + CurrentRoleMiddleware +│ ├── permissions.py # RBAC/ABAC 权限 +│ └── managers.py # UserManager + RoleManager + ShopManager + PositionManager +│ +├── msync/ # MSYNC 服务间通信 +│ ├── auth.py # 服务认证签名 +│ ├── registry.py # 服务注册中心 +│ ├── ogm.py # 全局对象映射 +│ ├── Transaction.py # 分布式事务 +│ ├── LocalTx.py # 本地事务 +│ └── client.py # 服务客户端 +│ +├── commission/ # 分佣引擎 +│ ├── models.py # 6 张数据模型 +│ ├── manager.py # CommissionEngine 编排器 +│ └── strategies/ +│ └── engine.py # 5 种策略 + 工厂 +│ +└── payment/ # 支付网关 + ├── models.py # 2 张数据模型 + ├── manager.py # PaymentManager 编排器 + └── gateways/ + ├── base.py # 抽象网关 + 数据类 + ├── registry.py # GatewayRegistry + payment_registry + ├── wechat.py # 微信支付适配器 + ├── alipay.py # 支付宝适配器 + └── bank.py # 银行卡适配器 +``` + +--- + +## 4. 用户管理 (UserManager) + +```python +from gvsdsdk.models import User + +# 创建用户 +user = User.objects.create_user(username='alice', password='xxx', email='a@b.com') +user = User.objects.create_superuser(username='admin', password='xxx') + +# 密码 +user.SetPassword('new_pass') # bcrypt 哈希 +user.CheckPassword('input') # 验证密码 + +# 查询 +user = User.objects.get(UserName='alice') +users = User.objects.filter(UserAccountLicense=1) + +# 属性 +user.IsActive # UserAccountLicense == 1 +user.IsOnJob # UserPositionStatus == 0 +user.UUIDStr # UUID 字符串 +user.HasPermission('menu:admin') # 检查权限 +``` + +--- + +## 5. 角色管理 (RoleManager) + +```python +from gvsdsdk.auth.managers import RoleManager + +# 创建角色 +role = RoleManager.CreateRole(name='管理员', company_uuid=company_uuid) + +# 分配/移除角色 +RoleManager.AssignRole(user_uuid=user.UserUUID, role_uuid=role.RoleUUID) +RoleManager.RemoveRole(user_uuid=user.UserUUID, role_uuid=role.RoleUUID) + +# 查询角色 +roles = RoleManager.GetRolesForUser(user_uuid=user.UserUUID) +users = RoleManager.GetUsersForRole(role_uuid=role.RoleUUID) + +# 分配/移除权限 +RoleManager.AssignPermission(role_uuid=role.RoleUUID, perm_code='menu:admin', perm_name='菜单管理') +RoleManager.RemovePermission(role_uuid=role.RoleUUID, perm_code='menu:admin') + +# 查询权限 +perms = RoleManager.GetPermissionsForRole(role_uuid=role.RoleUUID) +``` + +--- + +## 6. 商铺管理 (ShopManager) + +```python +from gvsdsdk.auth.managers import ShopManager + +# 创建商铺 +shop = ShopManager.CreateShop( + name='旗舰店', + company_uuid=company_uuid, + code='SHOP-001', + shop_type=1, + ShopAddress='北京市朝阳区xxx', + ShopContact='张三', + ShopPhone='13800138000', + ShopDesc='公司旗舰门店', +) + +# 角色关联商铺 +ShopManager.AssignRole(role_uuid=role.RoleUUID, shop_uuid=shop.ShopUUID, is_main_shop=True) +ShopManager.RemoveRole(role_uuid=role.RoleUUID, shop_uuid=shop.ShopUUID) + +# 查询 +shops = ShopManager.GetShopsForRole(role_uuid=role.RoleUUID) # 角色的所有商铺 +roles = ShopManager.GetRolesForShop(shop_uuid=shop.ShopUUID) # 商铺的所有角色 +main_shop = ShopManager.GetMainShopForRole(role_uuid=role.RoleUUID) # 角色的主商铺 +company_shops = ShopManager.GetShopsForCompany(company_uuid=company_uuid) # 公司的所有商铺 + +# 模型方法 +shop.GetRoles() # 商铺关联的所有角色 +``` + +### 商铺模型字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| ShopUUID | BINARY(16) PK | 商铺 UUID | +| CompanyUUID | BINARY(16) | 所属公司 UUID | +| ShopName | VARCHAR(128) | 商铺名称 | +| ShopCode | VARCHAR(64) | 商铺编码 | +| ShopType | SMALLINT | 商铺类型 | +| ShopStatus | SMALLINT | 状态(1=启用) | +| ShopAddress | VARCHAR(512) | 地址 | +| ShopContact | VARCHAR(128) | 联系人 | +| ShopPhone | VARCHAR(32) | 联系电话 | +| ShopDesc | VARCHAR(512) | 描述 | +| CreateTime | DATETIME | 创建时间 | +| UpdateTime | DATETIME | 更新时间 | + +### 角色商铺关联表 (RoleShop) + +| 字段 | 类型 | 说明 | +|------|------|------| +| RoleShopUUID | BINARY(16) PK | 关联 UUID | +| RoleUUID | BINARY(16) | 角色 UUID | +| ShopUUID | BINARY(16) | 商铺 UUID | +| IsMainShop | SMALLINT | 是否主商铺(0/1) | +| CreateTime | DATETIME | 创建时间 | + +--- + +## 7. 职务管理 (PositionManager) + +```python +from gvsdsdk.auth.managers import PositionManager + +# 创建职务 +position = PositionManager.CreatePosition( + name='总经理', + company_uuid=company_uuid, + code='POS-CEO', + level=10, + PositionDesc='公司最高管理职务', +) + +# 角色关联职务 +PositionManager.AssignRole(role_uuid=role.RoleUUID, position_uuid=position.PositionUUID, is_main_position=True) +PositionManager.RemoveRole(role_uuid=role.RoleUUID, position_uuid=position.PositionUUID) + +# 查询 +positions = PositionManager.GetPositionsForRole(role_uuid=role.RoleUUID) # 角色的所有职务 +roles = PositionManager.GetRolesForPosition(position_uuid=position.PositionUUID) # 职务的所有角色 +main_pos = PositionManager.GetMainPositionForRole(role_uuid=role.RoleUUID) # 角色的主职务 +company_positions = PositionManager.GetPositionsForCompany(company_uuid=company_uuid) # 公司的所有职务 + +# 模型方法 +position.GetRoles() # 职务关联的所有角色 +``` + +### 职务模型字段 + +| 字段 | 类型 | 说明 | +|------|------|------| +| PositionUUID | BINARY(16) PK | 职务 UUID | +| CompanyUUID | BINARY(16) | 所属公司 UUID | +| PositionName | VARCHAR(128) | 职务名称 | +| PositionCode | VARCHAR(64) | 职务编码 | +| PositionLevel | SMALLINT | 职务级别 | +| PositionStatus | SMALLINT | 状态(1=启用) | +| PositionDesc | VARCHAR(512) | 描述 | +| CreateTime | DATETIME | 创建时间 | +| UpdateTime | DATETIME | 更新时间 | + +### 角色职务关联表 (RolePosition) + +| 字段 | 类型 | 说明 | +|------|------|------| +| RolePositionUUID | BINARY(16) PK | 关联 UUID | +| RoleUUID | BINARY(16) | 角色 UUID | +| PositionUUID | BINARY(16) | 职务 UUID | +| IsMainPosition | SMALLINT | 是否主职务(0/1) | +| CreateTime | DATETIME | 创建时间 | + +--- + +## 8. 权限验证 (PermissionVerifier) + +```python +from gvsdsdk.auth.permissions import PermissionVerifier, IsAdminUser, CompanyScopedMixin + +# 手动验证 +if PermissionVerifier(perm_level=1, perm_attrs=['menu:admin'], user=request.user, request=request): + pass + +# DRF 权限类 +class MyViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated, IsAdminUser] + +# 公司范围自动过滤 +class MyCompanyViewSet(CompanyScopedMixin, viewsets.ModelViewSet): + company_field = 'CompanyUUID' # 自动按当前角色公司过滤 +``` + +--- + +## 9. ELT 认证 + +### 认证流程 + +``` +浏览器 Cookie (ELT_TOKEN) → ELTAuthentication → EphemeralToken → User +``` + +### 中间件 + +```python +# settings.py MIDDLEWARE +'gvsdsdk.auth.middleware.ELTAuthMiddleware', # 解析 ELT Cookie +'gvsdsdk.auth.middleware.CurrentRoleMiddleware', # 注入 request.current_role + +# 视图中使用 +request.user # gvsdsdk.models.User 实例 +request.current_role # gvsdsdk.models.Role 实例(或 None) +request.current_company_uuid # 当前角色所属公司 UUID(或 None) +``` + +### Cookie 管理 + +```python +from gvsdsdk.auth.cookie import SetELTCookie, DeleteELTCookie, ELT_COOKIE_NAME + +# 登录成功后设置 Cookie +SetELTCookie(response, token_value) + +# 登出时删除 Cookie +DeleteELTCookie(response) +``` + +### 加密工具 + +```python +from gvsdsdk.auth.crypto import HashPassword, VerifyPassword, SymmetricEncrypt, SymmetricDecrypt + +# 密码 +hashed = HashPassword('plaintext') +is_valid = VerifyPassword('plaintext', hashed) + +# AES-256-GCM 对称加密 +encrypted = SymmetricEncrypt('sensitive data', key) +decrypted = SymmetricDecrypt(encrypted, key) +``` + +--- + +## 10. 分佣引擎 (CommissionEngine) + +```python +from gvsdsdk.commission import CommissionEngine, CommissionError + +engine = CommissionEngine() + +# 配置规则 +engine.ConfigureRule( + event_type='order_complete', + participant_rules=[ + {'Type': 'percentage', 'Rate': 0.10, 'ParticipantID': 'platform'}, + {'Type': 'fixed', 'Amount': 50.00, 'ParticipantID': 'agent'}, + {'Type': 'remainder', 'ParticipantID': 'merchant'}, + ], +) + +# 触发分佣 +engine.Dispatch( + event_type='order_complete', + event_key='ORDER-12345', + total_amount=1000.00, + tenant_uuid=tenant_uuid, +) + +# 邀请链分佣 +engine.BuildInviteChainCommission( + event_type='member_purchase', + event_key='MEMBER-67890', + total_amount=500.00, + invite_chain=[user_uuid_1, user_uuid_2, user_uuid_3], + tenant_uuid=tenant_uuid, +) + +# 预结算 +engine.PreSettle(event_key='ORDER-12345', participant_id='agent', amount=50.00) +engine.ConfirmPreSettlement(presettlement_uuid=uuid) +engine.RejectPreSettlement(presettlement_uuid=uuid) + +# 查询 +records = engine.QueryRecords(participant_id='agent') +total = engine.TotalByParticipant(participant_id='agent') +``` + +### 策略类型 + +| 策略 | Type | 说明 | +|------|------|------| +| PercentageStrategy | `percentage` | 比例分账,支持保底/封顶 | +| FixedStrategy | `fixed` | 固定金额 | +| TieredStrategy | `tiered` | 阶梯分账 | +| RemainderStrategy | `remainder` | 剩余归平台 | +| WithdrawalFeeStrategy | `withdrawal_fee` | 提现手续费 | + +--- + +## 11. 支付网关 (PaymentManager) + +```python +from gvsdsdk.payment import PaymentManager, PaymentError + +manager = PaymentManager() + +# 配置网关 +manager.ConfigureGateway( + tenant_uuid=tenant_uuid, + platform='wechat', + app_id='wx1234567890', + merchant_id='1234567890', + api_key='***', +) + +# 发起支付 +response = manager.Pay( + tenant_uuid=tenant_uuid, + platform='wechat', + out_trade_no='ORDER-12345', + total_amount=100.00, + subject='商品名称', + pay_method='JSAPI', + openid='user_openid', +) + +# 查询订单 +result = manager.Query(tenant_uuid=tenant_uuid, out_trade_no='ORDER-12345') + +# 退款 +result = manager.Refund( + tenant_uuid=tenant_uuid, + out_trade_no='ORDER-12345', + refund_amount=50.00, + refund_reason='用户申请退款', +) + +# 企业转账 +result = manager.Transfer( + tenant_uuid=tenant_uuid, + platform='wechat', + out_trade_no='TRANSFER-001', + amount=100.00, + transfer_type='balance', + openid='user_openid', +) + +# 处理回调 +result = manager.HandleNotify(platform='wechat', raw_body=request.body) + +# 列出可用渠道 +channels = manager.ListChannels() +``` + +### 支付渠道 + +| 渠道 | Platform | 支付方式 | 转账 | +|------|----------|---------|------| +| 微信支付 | `wechat` | JSAPI/Native/H5/小程序 | 支持 | +| 支付宝 | `alipay` | APP/H5/Native | 支持 | +| 银行卡 | `bank` | APP/H5/Native | 仅 bank 类型 | + +--- + +## 12. MSYNC 服务间通信 + +```python +from gvsdsdk.msync import ServiceRegistry, OGMManager, TransactionManager, ServiceClient +from gvsdsdk.msync.auth import SignPayload, BuildAuthPayload + +# 服务注册 +registry = ServiceRegistry() +registry.Register( + service_name='my-service', + domain='https://my-service.gvsds.com', + secret='***', + scope=['ogm:read', 'ogm:write'], +) + +# OGM 全局对象映射 +ogm = OGMManager() +mapping_id = ogm.CreateMapping(local_type='User', local_id=user_id) +remote_obj = ogm.GetRemote(mapping_id) + +# 分布式事务 +tx_mgr = TransactionManager() +tx = tx_mgr.Begin(participants=['service-a', 'service-b']) +tx_mgr.ExecuteStep(tx, 'service-a', 'create_order', {'item': 'xxx'}) +tx_mgr.ExecuteStep(tx, 'service-b', 'deduct_stock', {'item': 'xxx'}) +tx_mgr.Commit(tx) + +# 服务客户端 +client = ServiceClient(service_name='target-service') +response = client.Call('/api/resource', method='GET') +``` + +--- + +## 13. FluentQuery ORM 查询 + +```python +from gvsdsdk.models import User, Role, Shop, Position + +# 通过 .query 属性使用 FluentQuery +users = User.query.filter(User.UserAccountLicense == 1).all() +admins = User.query.filter(User.IsSuperuser == True).all() + +# 运算符重载 +young = User.query.filter(User.UserAccountLicense != 0).all() + +# 排序和分页 +users = User.query.filter(User.IsActive == True).order_by(User.UserCreateTime).limit(10) + +# Django ORM 原生查询(始终可用) +users = User.objects.filter(UserAccountLicense=1) +users = User.objects.filter(UserName__contains='admin') +``` + +--- + +## 14. 数据库路由与建表 + +### SAASDatabaseRouter + +```python +# settings.py — 子服务器连接 SAAS 主库 +DATABASE_ROUTERS = ['gvsdsdk.db_router.SAASDatabaseRouter'] + +# 可选配置 +GVSDSDK_SAAS_DB_ALIAS = 'saas' # SAAS 数据库别名,默认 'saas' +GVSDSDK_SAAS_READ_ONLY = True # 子服务器只读,默认 True +``` + +### gvsdsdk_init 建表命令 + +```bash +# 完整建表 +python manage.py gvsdsdk_init --db default + +# 仅打印 SQL +python manage.py gvsdsdk_init --db default --dry-run + +# 按模块建表 +python manage.py gvsdsdk_init --db default --modules auth +python manage.py gvsdsdk_init --db default --modules auth,commission,payment +``` + +### GVSDSDK_MANAGE_TABLES + +```python +# settings.py — 同源数据库,允许 Django 管理表 +GVSDSDK_MANAGE_TABLES = True + +# 然后执行 +# python manage.py makemigrations gvsdsdk +# python manage.py migrate +``` + +--- + +## 15. 数据模型一览 + +### SAAS 核心模型 (gvsdsdk.models) + +| 模型 | 表名 | 说明 | +|------|------|------| +| User | User | 用户表 | +| Role | Role | 角色表 | +| Permission | Permission | 权限表 | +| UserRole | UserRole | 用户-角色多对多 | +| RoleDept | RoleDept | 角色-部门多对多 | +| Shop | Shop | 商铺表 | +| RoleShop | RoleShop | 角色-商铺多对多 | +| Position | Position | 职务表 | +| RolePosition | RolePosition | 角色-职务多对多 | +| EphemeralToken | EphemeralToken | ELT 令牌表 | +| User2FA | User2FA | 双因素认证表 | +| UserPending2FA | UserPending2FA | 2FA 待验证表 | +| UserAvatar | UserAvatar | 用户头像表 | +| UserLoginLog | UserLoginLog | 登录日志表 | +| AccountActivation | AccountActivation | 账户激活码表 | +| UserABAC | UserABAC | 用户 ABAC 表 | +| Password | Password | 密码表 | + +### 分佣模型 (gvsdsdk.commission.models) + +| 模型 | 表名 | 说明 | +|------|------|------| +| CommissionEvent | CommissionEvent | 分佣事件表 | +| RateTable | CommissionRateTable | 费率表 | +| RateEntry | CommissionRateEntry | 费率条目表 | +| CommissionRule | CommissionRule | 分佣规则表 | +| CommissionRecord | CommissionRecord | 分佣记录表 | +| PreSettlement | CommissionPreSettlement | 预结算表 | + +### 支付模型 (gvsdsdk.payment.models) + +| 模型 | 表名 | 说明 | +|------|------|------| +| PaymentGatewayConfig | PaymentGatewayConfig | 支付网关配置表 | +| PaymentTransaction | PaymentTransaction | 支付交易流水表 | + +--- + +## 导入速查表 + +| 用途 | 导入路径 | 顶层快捷 | +|------|---------|---------| +| 用户模型 | `from gvsdsdk.models import User` | `from gvsdsdk import User` | +| 角色管理 | `from gvsdsdk.auth.managers import RoleManager` | `from gvsdsdk import RoleManager` | +| 商铺管理 | `from gvsdsdk.auth.managers import ShopManager` | `from gvsdsdk import ShopManager` | +| 职务管理 | `from gvsdsdk.auth.managers import PositionManager` | `from gvsdsdk import PositionManager` | +| 权限验证 | `from gvsdsdk.auth.permissions import PermissionVerifier` | `from gvsdsdk import PermissionVerifier` | +| ELT 认证 | `from gvsdsdk.auth.elt_auth import ELTAuthentication` | `from gvsdsdk import ELTAuthentication` | +| Cookie | `from gvsdsdk.auth.cookie import SetELTCookie` | `from gvsdsdk import SetELTCookie` | +| 加密 | `from gvsdsdk.auth.crypto import SymmetricEncrypt` | `from gvsdsdk import SymmetricEncrypt` | +| 中间件 | `from gvsdsdk.auth.middleware import ELTAuthMiddleware` | `from gvsdsdk import ELTAuthMiddleware` | +| 分佣引擎 | `from gvsdsdk.commission import CommissionEngine` | `from gvsdsdk import CommissionEngine` | +| 支付网关 | `from gvsdsdk.payment import PaymentManager` | `from gvsdsdk import PaymentManager` | +| 微信支付 | `from gvsdsdk.payment.gateways.wechat import WechatPaymentGateway` | `from gvsdsdk import WechatPaymentGateway` | +| MSYNC 认证 | `from gvsdsdk.msync.auth import SignPayload` | `from gvsdsdk import SignPayload` | +| 服务注册 | `from gvsdsdk.msync import ServiceRegistry` | `from gvsdsdk import ServiceRegistry` | +| OGM | `from gvsdsdk.msync import OGMManager` | `from gvsdsdk import OGMManager` | +| 事务 | `from gvsdsdk.msync import TransactionManager` | `from gvsdsdk import TransactionManager` | +| 数据库路由 | `from gvsdsdk.db_router import SAASDatabaseRouter` | — | +| UUID 工具 | `from gvsdsdk.model_utils import UUIDObj` | `from gvsdsdk import UUIDObj` | +| QModel | `from gvsdsdk.model_base import QModel` | `from gvsdsdk import QModel` | diff --git a/gvsdsdk/INSTALL.md b/gvsdsdk/INSTALL.md new file mode 100644 index 0000000..a0b38f7 --- /dev/null +++ b/gvsdsdk/INSTALL.md @@ -0,0 +1,167 @@ +# 双端依赖安装指南 + +> 本节描述如何让 `gvsds` 主服务器和 `ATestCServer` 等子服务器**共享同一版本**的 `gvsdsdk`。 + +## 1. 版本一致性原则 + +- **同一代码**:双端导入的是**同一份 Python 文件**(物理位置 `D:\SMANAGE\gvsdsdk`) +- **同一版本**:通过 `pyproject.toml` 中的 `version = "1.0.0"` 锁定 +- **启动校验**:双端启动时调用 `gvsdsdk.check_min_version("1.0.0")`,版本不符会打 WARNING 日志 +- **热重载支持**:直接编辑 `D:\SMANAGE\gvsdsdk` 下的文件即可生效(无需 reinstall) + +## 2. 三种安装方式 + +### 方式 A:PYTHONPATH 方式(推荐开发用) + +```bash +# 启动 gvsds +cd D:\SMANAGE\gvsds +set PYTHONPATH=D:\SMANAGE\gvsds;D:\SMANAGE\gvsdsdk +python manage.py runserver + +# 启动 ATestCServer +cd D:\SMANAGE\ATestCServer +set PYTHONPATH=D:\SMANAGE\ATestCServer;D:\SMANAGE\gvsdsdk +python manage.py runserver 0.0.0.0:25080 +``` + +`manage.py` 和 `settings.py` 内已自动 `sys.path.append(...)`,无需手动设置。 + +### 方式 B:可编辑 pip 安装(推荐开发用,自动同步) + +```bash +# 全局安装一次即可,所有项目共享 +pip install -e D:\SMANAGE\gvsdsdk + +# 验证 +pip show gvsdsdk +# Name: gvsdsdk +# Version: 1.0.0 +# Location: D:\SMANAGE\gvsdsdk +``` + +### 方式 C:路径式 requirements 固定(推荐生产/部署) + +`gvsds/requirements.txt`: +``` +gvsdsdk==1.0.0 +``` + +`ATestCServer/requirements.txt`: +``` +gvsdsdk==1.0.0 +``` + +部署时 `pip install -r requirements.txt` 会自动从 PyPI 拉取对应版本(或私有源)。 + +## 3. 启动时的版本校验日志 + +**正常启动**(两端版本一致): + +``` +gvsds 端: +[INFO] gvsds.startup: [SDK] 加载: v1.0.0 @ D:\SMANAGE\gvsdsdk +[INFO] gvsds.startup: [SDK] OK: SDK 1.0.0 >= 1.0.0 + +ATestCServer 端: +[INFO] apps: [SDK] 加载: v1.0.0 @ D:\SMANAGE\gvsdsdk +[INFO] apps: [SDK] OK: SDK 1.0.0 >= 1.0.0 +``` + +**版本不一致**(双端版本不同): + +``` +[WARN] gvsds.startup: [SDK] 版本不符!FAIL: SDK 0.9.0 < 1.0.0 (required)。双端 gvsdsdk 版本必须一致! +``` + +## 4. CLI 验证工具 + +```bash +# 默认:显示简要信息 +python -m gvsdsdk + +# 仅版本号(适合脚本中调用) +python -m gvsdsdk --version +# 1.0.0 + +# 详细信息 +python -m gvsdsdk --info +# GVS-DSDK Version Information: +# Version : 1.0.0 +# Major : 1 +# Minor : 0 +# Patch : 0 +# Prerelease : +# Location : D:\SMANAGE\gvsdsdk +# Python : 3.12.0 +# Platform : Windows-10-... +# Machine : AMD64 + +# 最低版本检查(CI 友好) +python -m gvsdsdk --check 1.0.0 +# OK: SDK 1.0.0 >= 1.0.0 (返回 0) + +python -m gvsdsdk --check 2.0.0 +# FAIL: SDK 1.0.0 < 2.0.0 (required) (返回 1) + +# 列出所有公开符号 +python -m gvsdsdk --symbols + +# 测试连通性 +python -m gvsdsdk --ping +# Pong — SDK 1.0.0 ready at D:\SMANAGE\gvsdsdk +# 23 symbols available (lazy) +``` + +## 5. 升级 SDK 版本 + +当需要发布新版本时: + +1. **修改 `D:\SMANAGE\gvsdsdk\__init__.py`**: + ```python + VERSION_MAJOR = 1 + VERSION_MINOR = 1 # 升级 + VERSION_PATCH = 0 + ``` + +2. **修改 `D:\SMANAGE\gvsdsdk\pyproject.toml`**: + ```toml + [project] + version = "1.1.0" + ``` + +3. **同步更新双端 `requirements.txt`**: + ``` + gvsdsdk==1.1.0 + ``` + +4. **同步更新双端最低版本常量**: + - `gvsds/settings.py:17` → `_REQUIRED_GVSDSDK = '1.1.0'` + - `ATestCServer/apps/sub_server/apps.py:12` → `REQUIRED_GVSDSDK_VERSION = '1.1.0'` + +5. **重启双端** → 启动日志会报告新版本 + +## 6. 常见问题 + +| 现象 | 原因 | 解决 | +|---|---|---| +| `ImportError: No module named 'gvsdsdk'` | PYTHONPATH 未设置 | 安装 SDK 或加入路径 | +| `WARNING [SDK] 版本不符` | 双端版本不一致 | 升级较低端 | +| `WARNING [SDK] gvsdsdk 未安装` | 子服务器 PYTHONPATH 漏配 | 检查 `manage.py:5` | +| 编辑 SDK 后无变化 | 字节码缓存 | 删除 `__pycache__/` | + +## 7. 跨平台路径 + +`pyproject.toml` 路径引用应使用 `file:///` 协议: + +```toml +gvsdsdk @ file:///D:/SMANAGE/gvsdsdk ; sys_platform == "win32" +gvsdsdk @ file:///opt/gvsdsdk ; sys_platform == "linux" +gvsdsdk @ file:///usr/local/gvsdsdk ; sys_platform == "darwin" +``` + +或在 CI 中使用环境变量: + +```bash +pip install -e "$GVS_SDK_PATH" +``` diff --git a/gvsdsdk/README.md b/gvsdsdk/README.md new file mode 100644 index 0000000..4b64377 --- /dev/null +++ b/gvsdsdk/README.md @@ -0,0 +1,376 @@ +# GVS-DSDK — GVS Distributed SDK + +> **薄封装层**:直接包装 `gvsds/apps/msync` 原生层,作为子服务器(如 `ATestCServer`)和主服务器(`gvsds`)之间共享的通信桥梁。 +> +> **设计原则**:所有方法名 1:1 对应原生层 PascalCase;零业务逻辑;纯转发。 +> +> **双端版本严格一致**:主从两端必须使用同一 SDK 版本(当前 `1.0.0`),通过 `pyproject.toml` 锁定。 + +--- + +## 1. 简介 + +`gvsdsdk` 把 `gvsds/apps/msync` 的**所有公共类/函数**原样转发导出,使得: + +- **主服务器**(`gvsds`):可继续使用自己的 `gvsds.apps.msync` 模块 +- **子服务器**(`ATestCServer`):可从 `gvsdsdk` 导入**完全相同的 API**,但**不依赖 gvsds 模型层** +- **第三方服务**:可以独立引入 `gvsdsdk` 即可使用全部 MSYNC 能力 + +被转发的原生模块: + +| SDK 模块 | 包装的原生模块 | 提供的类/函数 | +|---|---|---| +| `gvsdsdk.auth` | `gvsds.apps.msync.auth` | `SignPayload`, `GenerateChallenge`, `BuildAuthPayload`, `AuthenticateService`, `ServiceTokenEncoder`, … | +| `gvsdsdk.registry` | `gvsds.apps.msync.registry` | `ServiceRegistry` | +| `gvsdsdk.ogm` | `gvsds.apps.msync.ogm` | `OGMManager` | +| `gvsdsdk.transaction` | `gvsds.apps.msync.transaction` | `TransactionManager` | +| `gvsdsdk.local_tx` | `gvsds.apps.msync.local_tx` | `LocalTransactionManager`, `StepHandler`, `local_tx` | +| `gvsdsdk.client` | `gvsds.apps.msync.client` | `ServiceClient` | + +每个类在 SDK 与原生层是**完全同一个对象**(`is` 比较为 `True`),无任何运行时包装开销。 + +## 2. 安装 + +子服务器(如 ATestCServer)项目根目录的 `manage.py` 已自动添加 `gvsdsdk` 的父目录到 `sys.path`: + +```python +# manage.py:5 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +``` + +无需额外配置。 + +**独立使用**(非 gvsds 生态项目): + +```bash +$env:PYTHONPATH = "D:\SMANAGE\gvsdsdk" +# 或 Linux/macOS +export PYTHONPATH=/path/to/SMANAGE/gvsdsdk +``` + +依赖: +- **核心**:`gvsds.apps.msync`(位于 `gvsds/` 项目内) +- **Python**:仅使用标准库 + +## 3. 目录结构 + +``` +gvsdsdk/ +├── __init__.py # 公共导出(re-export) +├── auth.py # → gvsds.apps.msync.auth +├── registry.py # → gvsds.apps.msync.registry +├── ogm.py # → gvsds.apps.msync.ogm +├── transaction.py # → gvsds.apps.msync.transaction +├── local_tx.py # → gvsds.apps.msync.local_tx +└── client.py # → gvsds.apps.msync.client +``` + +每个文件只有 5-25 行代码,纯粹是 `from gvsds.apps.msync.X import Y` 的重新导出。 + +## 4. 核心 API(PascalCase) + +### 4.1 认证 (gvsdsdk.auth) + +| 函数 / 类 | 描述 | +|---|---| +| `SignPayload(payload, secret)` | HMAC-SHA256 签名 | +| `VerifySignature(payload, signature, secret)` | 验签 | +| `GenerateChallenge()` | 生成挑战字符串 | +| `BuildAuthPayload(service, challenge, ts=None)` | 构造认证载荷 | +| `SignAuthPayload(payload, secret)` | 一站式签名 | +| `VerifyAuthPayload(payload, signature, secret)` | 一站式验签 | +| `ServiceTokenEncoder` | JWT 风格令牌编解码 | +| `IssueServiceToken(service_node, ...)` | 签发令牌 | +| `AuthenticateService(service_name, signed_payload, signature)` | 完整认证 | +| `ValidateAccessToken(access_token)` | 校验访问令牌 | +| `RefreshServiceToken(refresh_token)` | 刷新令牌 | + +### 4.2 注册中心 (gvsdsdk.registry) + +```python +from gvsdsdk import ServiceRegistry + +node = ServiceRegistry.Register( + Name='my-service', + Domain='http://127.0.0.1:8080', + Secret='shared-secret', + Scope=['ogm:read', 'ogm:write'], + ServiceType=0, # 0=Worker, 1=Master + Metadata={'region': 'cn-east'}, +) + +ServiceRegistry.Heartbeat('my-service') +ServiceRegistry.Discover('my-service') +ServiceRegistry.DiscoverAll(ActiveOnly=True) +ServiceRegistry.UpdateScope('my-service', ['ogm:read']) +ServiceRegistry.RotateSecret('my-service', 'new-secret') +ServiceRegistry.Deregister('my-service') +``` + +### 4.3 OGM (gvsdsdk.ogm) + +```python +from gvsdsdk import OGMManager + +entry = OGMManager.Register( + ObjectType='user', + LocalID='123', + ServiceName='my-service', + GlobalID=None, # 可选,自动生成 + Metadata={'username': 'tom'}, +) + +info = OGMManager.Resolve(entry['GlobalID']) +local = OGMManager.ResolveLocal('user', '123', 'my-service') +by_type = OGMManager.QueryByType('user') +by_svc = OGMManager.QueryByService('my-service') +OGMManager.Update(entry['GlobalID'], metadata={'vip': True}) +OGMManager.Deregister(entry['GlobalID']) +``` + +### 4.4 分布式事务 (gvsdsdk.transaction) + +```python +from gvsdsdk import TransactionManager + +dtx = TransactionManager.Create( + Name='create-order', + StepsData=[ + {'ServiceName': 'svc-a', 'StepName': 'create_user', 'Payload': {...}}, + {'ServiceName': 'svc-b', 'StepName': 'create_wallet', 'Payload': {...}}, + ], + Description='创建订单事务', + TransactionType='saga', + InitiatorServiceName='my-service', + TimeoutSeconds=300, +) + +TransactionManager.Execute(dtx.TransactionCode, access_token=token) +TransactionManager.Compensate(dtx.TransactionCode) +TransactionManager.Cancel(dtx.TransactionCode) +TransactionManager.GetDetail(dtx.TransactionCode) +TransactionManager.RetryStep(dtx.TransactionCode, step_index=0) +TransactionManager.CheckTimeouts() +TransactionManager.QueryByStatus(status=2) # Committed +``` + +### 4.5 本地事务 (gvsdsdk.local_tx) + +```python +from gvsdsdk import local_tx + +def create_user(payload): + user_id = do_something(payload) + return {'UserID': user_id} + +def delete_user(payload, result): + undo_something(result['UserID']) + +local_tx.register('create_user', action=create_user, compensate=delete_user) +``` + +### 4.6 服务客户端 (gvsdsdk.client) + +```python +from gvsdsdk import ServiceClient + +client = ServiceClient( + ServiceName='my-service', + ServiceSecret='shared-secret', + MasterURL='https://api.gvsds.com', +) + +client.Authenticate() +info = client.DiscoverService('gerp') +ogm = client.ResolveOGM('user:abc123') +resp = client.Request('GET', 'https://gerp.gvsds.com/api/x', scope=['ogm:read']) +``` + +## 5. Socket.IO 事件协议(PascalCase) + +> **重要**:所有事件的请求/响应字段全部使用 **PascalCase**。 + +| 事件 | 请求字段 | 响应字段 | +|---|---|---| +| `msync:register` | `ServiceName`, `Domain`, `Secret`, `Scope`, `ServiceType`, `Metadata` | `ServiceNode` 序列化 | +| `msync:auth` | `ServiceName`, `Payload`, `Signature` | `AccessToken`, `RefreshToken`, `Scope`, `ExpiresAt` | +| `msync:auth:refresh` | `RefreshToken` | 同上 | +| `msync:heartbeat` | — | — | +| `msync:deregister` | `ServiceName` | — | +| `msync:discover` | `ActiveOnly` | `services[]` | +| `msync:discover:one` | `ServiceName` | `service` | +| `msync:ogm:register` | `ObjectType`, `LocalID`, `ServiceName`, `GlobalID`, `Metadata` | `ogm` | +| `msync:ogm:batch` | `Items[]`, `ServiceName` | `ogms[]` | +| `msync:ogm:resolve` | `GlobalID` | `ogm` | +| `msync:ogm:query` | `ObjectType`/`ServiceName`/`LocalID` | `results` | +| `msync:ogm:update` | `GlobalID`, `Metadata` | `ogm` | +| `msync:ogm:deregister` | `GlobalID` | — | +| `msync:tx:create` | `Name`, `Steps[]`, `Description`, `TransactionType`, `InitiatorServiceName`, `ReferenceOGMUUID`, `Payload`, `TimeoutSeconds` | `dtx` | +| `msync:tx:execute` | `TransactionCode`, `AccessToken` | `dtx` | +| `msync:tx:compensate` | 同上 | `dtx` | +| `msync:tx:cancel` | `TransactionCode` | — | +| `msync:tx:detail` | `TransactionCode` | `dtx` | +| `msync:tx:retry` | `TransactionCode`, `StepIndex`, `AccessToken` | `step` | +| `msync:tx:timeout_check` | — | `TimedOut[]` | +| `msync:tx:query` | `Status` | `dtxs[]` | + +服务端推送: + +- `msync:service:online` — `{Service, Domain}` +- `msync:service:offline` — `{Service}` + +## 6. 快速上手 + +### 6.1 子服务器(ATestCServer)使用 SDK + +[ATestCServer/msync_client/sio_client.py](file:///d:/SMANAGE/ATestCServer/msync_client/sio_client.py) 提供了 `MSyncSocketClient`,**已全部 PascalCase**: + +```python +from msync_client.sio_client import MSyncSocketClient + +msync = MSyncSocketClient( + master_url='https://www.gvsds.com', + service_name='atest-cserver', + service_domain='http://127.0.0.1:25080', + service_secret='shared-secret', + scope=['ogm:read', 'ogm:write', 'registry:read'], +) +msync.Connect() + +# OGM +ogm = msync.OGMRegister('user', '123', 'atest-cserver') +resolved = msync.OGMResolve(ogm['GlobalID']) + +# 服务发现 +for svc in msync.DiscoverAll(): + print(svc['ServiceName'], '->', svc['Domain']) + +# 状态 +print('已认证:', msync.IsAuthenticated) +print('AccessToken:', msync.AccessToken[:20], '...') + +msync.Disconnect() +``` + +### 6.2 直接使用 SDK 类 + +```python +from gvsdsdk import ServiceRegistry, OGMManager, TransactionManager + +# 注册中心 +ServiceRegistry.Register( + Name='my-service', + Domain='http://localhost:8080', + Secret='secret', +) + +# OGM +entry = OGMManager.Register('user', '1', 'my-service') + +# 事务 +dtx = TransactionManager.Create( + Name='test-tx', + StepsData=[{'ServiceName': 'svc-a', 'StepName': 'step1'}], +) +``` + +## 7. 架构示意 + +``` +┌──────────────────┐ ┌──────────────────┐ +│ ATestCServer │ │ gvsds (master) │ +│ │ │ │ +│ MSyncSocketClient│ ←──┐ │ Socket.IO │ +│ (PascalCase) │ │ │ Server │ +│ │ │ │ (PascalCase) │ +│ ┌────────────┐ │ │ │ │ +│ │ gvsdsdk │ │ │ │ ┌────────────┐ │ +│ │ .auth │◄─┼─────┼───┼──│ msync │ │ +│ │ .ogm │ │ │ │ │ .auth │ │ +│ │ .registry │ │ │ │ │ .ogm │ │ +│ │ .tx │ │ │ │ │ .registry │ │ +│ │ .client │ │ │ │ │ .tx │ │ +│ └─────┬──────┘ │ │ │ └─────┬──────┘ │ +│ │ │ │ │ │ │ +│ 薄封装 │ │ │ 原生层 │ +│ ▼ │ │ │ ▼ │ +│ ┌────────────┐ │ │ │ ┌────────────┐ │ +│ │ gvsds │ │ │ │ │ gvsds │ │ +│ │ .apps │◄─┼─────┘ │ │ .apps │ │ +│ │ .msync │ │ │ │ .msync │ │ +│ └────────────┘ │ │ └────────────┘ │ +└──────────────────┘ └──────────────────┘ +``` + +`gvsdsdk` 与 `gvsds.apps.msync` 中的类是**完全同一个对象**——`from gvsdsdk import ServiceRegistry` 与 `from gvsds.apps.msync.registry import ServiceRegistry` 拿到的是同一个类。 + +## 8. 与项目原生层的集成 + +### 8.1 ATestCServer + +[ATestCServer/manage.py:5](file:///d:/SMANAGE/ATestCServer/manage.py#L5) 在启动时设置 `sys.path`: + +```python +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +# → D:\SMANAGE 被加入 sys.path +# → import gvsdsdk 即可工作 +``` + +子服务器**不直接 import** `gvsds.apps.msync`(避免 Django settings 依赖),仅通过 SDK 引用。 + +### 8.2 gvsds 主服务器 + +[gvsds/settings.py](file:///d:/SMANAGE/gvsds/settings.py) 启动时同样将 `gvsdsdk` 加入 `sys.path`: + +```python +# 在 wsgi.py / asgi.py 中 +sys.path.append(BASE_DIR) +``` + +`gvsds.apps.msync` 内部模块(如 [sio.py](file:///d:/SMANAGE/gvsds/apps/msync/sio.py))会**直接 import** 原生层 `gvsds.apps.msync.auth/registry/ogm/transaction`,**协议 100% 兼容**。 + +## 9. 测试与验证 + +```python +import django; django.setup() +import gvsdsdk + +# 验证 SDK 类与原生层是同一对象 +from gvsdsdk import ServiceRegistry, OGMManager, TransactionManager +from gvsds.apps.msync.registry import ServiceRegistry as NativeSR +from gvsds.apps.msync.ogm import OGMManager as NativeOGM +from gvsds.apps.msync.transaction import TransactionManager as NativeTM + +assert ServiceRegistry is NativeSR +assert OGMManager is NativeOGM +assert TransactionManager is NativeTM +print('SDK 薄封装校验通过') +``` + +## 10. 版本与兼容性 + +| 项目 | 版本 | +|---|---| +| **gvsdsdk** | 1.0.0 | +| **协议版本** | v1.0 (PascalCase 严格模式) | +| **Python** | >= 3.8 | +| **兼容 gvsds** | >= 1.0 | +| **兼容 ATestCServer** | >= 1.0 | + +## 11. 错误码速查 + +| 错误码 | 含义 | 建议处理 | +|---|---|---| +| 400 | 参数错误 | 检查请求体字段名(PascalCase) | +| 401 | 认证失败 | 检查 `ServiceSecret` | +| 404 | 资源不存在 | 重新注册服务 | +| 409 | 冲突 | OGM 已注册、服务重名等 | +| 500 | 服务器内部错误 | 查看服务器日志 | + +## 12. 维护 + +- **主服务器**:`gvsds/apps/msync/`(业务实现) +- **SDK 仓库**:`gvsdsdk/`(薄封装) +- **子服务器示例**:`ATestCServer/msync_client/` + +修改原生层时,**SDK 无需改动**——所有类/函数会通过 `from … import …` 自动反映最新行为。 diff --git a/gvsdsdk/__init__.py b/gvsdsdk/__init__.py new file mode 100644 index 0000000..a26c2dc --- /dev/null +++ b/gvsdsdk/__init__.py @@ -0,0 +1,340 @@ +"""GVS-DSDK — GVS Distributed SDK + +GVS 分布式服务间通信 SDK,作为子服务器(如 ATestCServer)和主服务器(gvsds) +之间的统一通信桥梁。 + +设计原则: + 1. 薄封装:直接包装 gvsds.apps.msync 原生层 + 2. 纯转发:方法签名 1:1 对应原生层 + 3. PascalCase:全部公开 API 使用 PascalCase + 4. 协议兼容:完全兼容 gvsds/apps/msync 现有协议 + 5. 惰性加载:仅在访问属性时才 import 原生层(避免 Django 未初始化时报错) + +包结构: + gvsdsdk/ + ├── auth/ # ELT 用户认证模块 + │ ├── elt_auth.py # ELT 认证后端 + │ ├── cookie.py # Cookie 管理 + │ ├── crypto.py # 密码哈希 + AES 加密 + │ ├── middleware.py # ELTAuthMiddleware + CurrentRoleMiddleware + │ ├── permissions.py # RBAC/ABAC 权限 + │ └── managers.py # UserManager + RoleManager + ├── msync/ # MSYNC 服务间通信 + ├── commission/ # 分佣引擎 + │ ├── models.py # 分佣数据模型 + │ ├── manager.py # CommissionEngine 编排器 + │ └── strategies/ # 分账策略 + ├── payment/ # 支付网关 + │ ├── models.py # 支付数据模型 + │ ├── manager.py # PaymentManager 编排器 + │ └── gateways/ # 支付渠道适配器 + ├── order/ # 订单模板与订单 + │ └── models.py # 订单数据模型 + ├── models.py # SAAS 核心数据模型 + ├── model_base.py # QModel + ├── model_utils.py # UUIDObj + └── fluent.py # FluentQuery ORM 查询层 + +使用示例: + from gvsdsdk import ServiceRegistry, OGMManager, TransactionManager + from gvsdsdk.auth import ELTAuthentication, PermissionVerifier + from gvsdsdk.commission import CommissionEngine + from gvsdsdk.payment import PaymentManager + print(gvsdsdk.__version__) # '1.0.0' +""" + +import os as _os +import sys as _sys +import platform as _platform +from typing import Optional, Tuple + + +# === 版本定义(单一来源) === + +#: 主版本号 +VERSION_MAJOR = 1 +#: 次版本号 +VERSION_MINOR = 0 +#: 补丁版本号 +VERSION_PATCH = 0 +#: 预发布标签(如 'a1', 'b2', 'rc1') +VERSION_PRERELEASE = '' + +#: 完整版本字符串(PEP 440 兼容) +__version__ = f'{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_PATCH}{VERSION_PRERELEASE}' + + +# === 包元数据 === + +#: 物理安装路径(pkg.__file__ 所在目录) +_PKG_DIR = _os.path.dirname(_os.path.abspath(__file__)) + + +def get_version() -> str: + """返回 SDK 版本字符串""" + return __version__ + + +def get_version_info() -> dict: + """返回详细版本信息字典 + + Returns: + 包含 version, location, python_version, platform 等字段 + """ + return { + 'Version': __version__, + 'Major': VERSION_MAJOR, + 'Minor': VERSION_MINOR, + 'Patch': VERSION_PATCH, + 'Prerelease': VERSION_PRERELEASE, + 'Location': _PKG_DIR, + 'Python': _sys.version.split()[0], + 'Platform': _platform.platform(), + 'Machine': _platform.machine(), + } + + +def check_min_version(min_version: str) -> Tuple[bool, str]: + """检查当前版本是否 >= 给定最低版本 + + Args: + min_version: 形如 '1.0.0' 的最低版本字符串 + + Returns: + (is_ok, message) 元组 + """ + def parse(v: str): + parts = v.split('.') + major = int(parts[0]) if len(parts) > 0 else 0 + minor = int(parts[1]) if len(parts) > 1 else 0 + patch_str = parts[2] if len(parts) > 2 else '0' + digits = '' + for c in patch_str: + if c.isdigit(): + digits += c + else: + break + patch = int(digits) if digits else 0 + return (major, minor, patch) + + current = parse(__version__) + required = parse(min_version) + + if current >= required: + return True, f'OK: SDK {__version__} >= {min_version}' + return False, f'FAIL: SDK {__version__} < {min_version} (required)' + + +# === 惰性加载(PEP 562)=== + +def _load_msync(): + from gvsdsdk import msync as _mod + return _mod + + +def _load_auth(): + from gvsdsdk import auth as _mod + return _mod + + +def _load_commission(): + from gvsdsdk import commission as _mod + return _mod + + +def _load_payment(): + from gvsdsdk import payment as _mod + return _mod + + +def _load_order(): + from gvsdsdk import order as _mod + return _mod + + +def _load_models(): + from gvsdsdk import models as _mod + return _mod + + +def _load_fluent(): + from gvsdsdk import fluent as _mod + return _mod + + +def _load_model_base(): + from gvsdsdk import model_base as _mod + return _mod + + +def _load_model_utils(): + from gvsdsdk import model_utils as _mod + return _mod + + +_LAZY_SYMBOLS = { + # ── MSYNC 服务间通信 ────────────────────────────────── + 'SignPayload': ('msync', 'SignPayload'), + 'VerifySignature': ('msync', 'VerifySignature'), + 'GenerateChallenge': ('msync', 'GenerateChallenge'), + 'BuildAuthPayload': ('msync', 'BuildAuthPayload'), + 'SignAuthPayload': ('msync', 'SignAuthPayload'), + 'VerifyAuthPayload': ('msync', 'VerifyAuthPayload'), + 'ServiceTokenEncoder': ('msync', 'ServiceTokenEncoder'), + 'AuthenticateService': ('msync', 'AuthenticateService'), + 'RefreshServiceToken': ('msync', 'RefreshServiceToken'), + 'ValidateAccessToken': ('msync', 'ValidateAccessToken'), + 'IssueServiceToken': ('msync', 'IssueServiceToken'), + 'ServiceRegistry': ('msync', 'ServiceRegistry'), + 'RegistryError': ('msync', 'RegistryError'), + 'OGMManager': ('msync', 'OGMManager'), + 'OGMError': ('msync', 'OGMError'), + 'TransactionManager': ('msync', 'TransactionManager'), + 'TransactionError': ('msync', 'TransactionError'), + 'StepExecutionError': ('msync', 'StepExecutionError'), + 'LocalTransactionManager': ('msync', 'LocalTransactionManager'), + 'StepHandler': ('msync', 'StepHandler'), + 'local_tx': ('msync', 'local_tx'), + 'ServiceClient': ('msync', 'ServiceClient'), + 'ServiceClientError': ('msync', 'ServiceClientError'), + + # ── ELT 用户认证 ────────────────────────────────────── + 'ELTAuthentication': ('auth', 'ELTAuthentication'), + 'ELT_COOKIE_NAME': ('auth', 'ELT_COOKIE_NAME'), + 'ELT_COOKIE_DOMAIN': ('auth', 'ELT_COOKIE_DOMAIN'), + 'ELT_COOKIE_MAX_AGE': ('auth', 'ELT_COOKIE_MAX_AGE'), + 'SetELTCookie': ('auth', 'SetELTCookie'), + 'DeleteELTCookie': ('auth', 'DeleteELTCookie'), + 'HashPassword': ('auth', 'HashPassword'), + 'VerifyPassword': ('auth', 'VerifyPassword'), + 'SymmetricEncrypt': ('auth', 'SymmetricEncrypt'), + 'SymmetricDecrypt': ('auth', 'SymmetricDecrypt'), + 'FileSHA1': ('auth', 'FileSHA1'), + 'FileSHA256': ('auth', 'FileSHA256'), + 'BytesSHA1': ('auth', 'BytesSHA1'), + 'BytesSHA256': ('auth', 'BytesSHA256'), + 'ELTAuthMiddleware': ('auth', 'ELTAuthMiddleware'), + 'CurrentRoleMiddleware': ('auth', 'CurrentRoleMiddleware'), + 'IsAdminUser': ('auth', 'IsAdminUser'), + 'IsCompanyMember': ('auth', 'IsCompanyMember'), + 'CompanyScopedMixin': ('auth', 'CompanyScopedMixin'), + 'PermissionVerifier': ('auth', 'PermissionVerifier'), + 'RoleManager': ('auth', 'RoleManager'), + 'ShopManager': ('auth', 'ShopManager'), + 'PositionManager': ('auth', 'PositionManager'), + + # ── 分佣引擎 ────────────────────────────────────────── + 'CommissionEngine': ('commission', 'CommissionEngine'), + 'CommissionError': ('commission', 'CommissionError'), + 'CommissionEvent': ('commission', 'CommissionEvent'), + 'RateTable': ('commission', 'RateTable'), + 'RateEntry': ('commission', 'RateEntry'), + 'CommissionRule': ('commission', 'CommissionRule'), + 'CommissionRecord': ('commission', 'CommissionRecord'), + 'PreSettlement': ('commission', 'PreSettlement'), + 'CreateStrategy': ('commission', 'CreateStrategy'), + 'PercentageStrategy': ('commission', 'PercentageStrategy'), + 'FixedStrategy': ('commission', 'FixedStrategy'), + 'TieredStrategy': ('commission', 'TieredStrategy'), + 'RemainderStrategy': ('commission', 'RemainderStrategy'), + 'WithdrawalFeeStrategy': ('commission', 'WithdrawalFeeStrategy'), + + # ── 支付网关 ────────────────────────────────────────── + 'PaymentManager': ('payment', 'PaymentManager'), + 'PaymentError': ('payment', 'PaymentError'), + 'PaymentGatewayConfig': ('payment', 'PaymentGatewayConfig'), + 'PaymentTransaction': ('payment', 'PaymentTransaction'), + 'EncryptField': ('payment', 'EncryptField'), + 'PaymentCredentials': ('payment', 'PaymentCredentials'), + 'PaymentRequest': ('payment', 'PaymentRequest'), + 'PaymentResponse': ('payment', 'PaymentResponse'), + 'RefundRequest': ('payment', 'RefundRequest'), + 'RefundResponse': ('payment', 'RefundResponse'), + 'TransferRequest': ('payment', 'TransferRequest'), + 'TransferResponse': ('payment', 'TransferResponse'), + 'QueryResponse': ('payment', 'QueryResponse'), + 'AbstractPaymentGateway': ('payment', 'AbstractPaymentGateway'), + 'GatewayRegistry': ('payment', 'GatewayRegistry'), + 'payment_registry': ('payment', 'payment_registry'), + 'WechatPaymentGateway': ('payment', 'WechatPaymentGateway'), + 'AlipayPaymentGateway': ('payment', 'AlipayPaymentGateway'), + 'BankPaymentGateway': ('payment', 'BankPaymentGateway'), + + # ── 订单模板与订单 ──────────────────────────────────── + 'OrderTemplate': ('order', 'OrderTemplate'), + 'OrderTemplateItem': ('order', 'OrderTemplateItem'), + 'OrderLevel': ('order', 'OrderLevel'), + 'Order': ('order', 'Order'), + 'OrderItem': ('order', 'OrderItem'), + + # ── 数据模型 ────────────────────────────────────────── + 'User': ('models', 'User'), + 'Role': ('models', 'Role'), + 'Permission': ('models', 'Permission'), + 'UserRole': ('models', 'UserRole'), + 'UUIDField': ('models', 'UUIDField'), + 'UserManager': ('models', 'UserManager'), + 'EphemeralToken': ('models', 'EphemeralToken'), + 'User2FA': ('models', 'User2FA'), + 'UserPending2FA': ('models', 'UserPending2FA'), + 'UserAvatar': ('models', 'UserAvatar'), + 'UserLoginLog': ('models', 'UserLoginLog'), + 'AccountActivation': ('models', 'AccountActivation'), + 'RolePermission': ('models', 'RolePermission'), + 'UserABAC': ('models', 'UserABAC'), + 'Password': ('models', 'Password'), + 'Shop': ('models', 'Shop'), + 'RoleShop': ('models', 'RoleShop'), + 'Position': ('models', 'Position'), + 'PositionTemplate': ('models', 'PositionTemplate'), + 'PositionTemplatePermission': ('models', 'PositionTemplatePermission'), + 'UserDept': ('models', 'UserDept'), + 'FixedBinaryField': ('models', 'FixedBinaryField'), + 'QModel': ('model_base', 'QModel'), + 'QModelBase': ('model_base', 'QModelBase'), + + # ── ORM 查询层 ──────────────────────────────────────── + 'FluentQuery': ('fluent', 'FluentQuery'), + 'Session': ('fluent', 'Session'), + 'db': ('fluent', 'db'), + 'func': ('fluent', 'func'), + 'FQ': ('fluent', 'FQ'), + 'FieldExpression': ('model_base', 'FieldExpression'), + + # ── 工具 ────────────────────────────────────────────── + 'UUIDObj': ('model_utils', 'UUIDObj'), +} + +_LOADERS = { + 'msync': _load_msync, + 'auth': _load_auth, + 'commission': _load_commission, + 'payment': _load_payment, + 'order': _load_order, + 'models': _load_models, + 'fluent': _load_fluent, + 'model_base': _load_model_base, + 'model_utils': _load_model_utils, +} + + +def __getattr__(name): + """惰性加载——仅在被访问时才 import 真正的模块""" + if name in _LAZY_SYMBOLS: + module_name, attr = _LAZY_SYMBOLS[name] + mod = _LOADERS[module_name]() + value = getattr(mod, attr) + globals()[name] = value + return value + raise AttributeError(f"module 'gvsdsdk' has no attribute {name!r}") + + +__all__ = list(_LAZY_SYMBOLS.keys()) + [ + 'get_version', + 'get_version_info', + 'check_min_version', + '__version__', + 'VERSION_MAJOR', + 'VERSION_MINOR', + 'VERSION_PATCH', +] diff --git a/gvsdsdk/__main__.py b/gvsdsdk/__main__.py new file mode 100644 index 0000000..977e112 --- /dev/null +++ b/gvsdsdk/__main__.py @@ -0,0 +1,76 @@ +"""gvsdsdk CLI 入口 + +用法: + python -m gvsdsdk # 默认:显示版本信息 + python -m gvsdsdk --version # 仅版本字符串 + python -m gvsdsdk --info # 详细版本信息 + python -m gvsdsdk --check 1.0.0 # 检查最低版本 + python -m gvsdsdk --symbols # 列出所有公开符号 + python -m gvsdsdk --ping # 测试 SDK 可用性 +""" + +import sys +import argparse + +# 当作为 __main__ 运行时,需显式 import 包(runpy 不会自动注入) +import gvsdsdk +from gvsdsdk import __version__, get_version_info, check_min_version, _PKG_DIR, _LAZY_SYMBOLS + + +def main(): + parser = argparse.ArgumentParser( + prog='gvsdsdk', + description='GVS-DSDK — 分布式服务通信 SDK', + ) + parser.add_argument('--version', action='store_true', help='显示版本号') + parser.add_argument('--info', action='store_true', help='显示详细信息') + parser.add_argument('--check', metavar='VERSION', help='检查最低版本要求') + parser.add_argument('--symbols', action='store_true', help='列出所有公开符号') + parser.add_argument('--ping', action='store_true', help='测试 SDK 可用性') + args = parser.parse_args() + + if args.version: + print(__version__) + return 0 + + if args.check: + ok, msg = check_min_version(args.check) + print(msg) + return 0 if ok else 1 + + if args.symbols: + from gvsdsdk import _LAZY_SYMBOLS + print(f'GVS-DSDK {__version__} — {len(_LAZY_SYMBOLS)} lazy symbols:') + by_mod = {} + for sym, (mod, _) in _LAZY_SYMBOLS.items(): + by_mod.setdefault(mod, []).append(sym) + for mod, syms in sorted(by_mod.items()): + print(f' [{mod}]') + for s in sorted(syms): + print(f' - {s}') + return 0 + + if args.ping: + from gvsdsdk import get_version_info, _LAZY_SYMBOLS + info = get_version_info() + print(f'Pong — SDK {info["Version"]} ready at {info["Location"]}') + print(f' {len(_LAZY_SYMBOLS)} symbols available (lazy)') + return 0 + + if args.info: + from gvsdsdk import get_version_info + info = get_version_info() + print('GVS-DSDK Version Information:') + for k, v in info.items(): + print(f' {k:12s} : {v}') + return 0 + + # 默认行为:简要版本信息 + print(f'GVS-DSDK {__version__}') + print(f' Location: {_PKG_DIR}') + print(f' Use --info for details, --help for all commands') + return 0 + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/gvsdsdk/apps.py b/gvsdsdk/apps.py new file mode 100644 index 0000000..812194a --- /dev/null +++ b/gvsdsdk/apps.py @@ -0,0 +1,13 @@ +"""GVS-DSDK Django AppConfig + +使 gvsdsdk 可作为 Django INSTALLED_APPS 注册,以便 Django 发现 SDK 中的模型。 +""" + +from django.apps import AppConfig + + +class GvsdsdkConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'gvsdsdk' + label = 'gvsdsdk' + verbose_name = 'GVS-DSDK' \ No newline at end of file diff --git a/gvsdsdk/auth/__init__.py b/gvsdsdk/auth/__init__.py new file mode 100644 index 0000000..d6b84c1 --- /dev/null +++ b/gvsdsdk/auth/__init__.py @@ -0,0 +1,50 @@ +"""gvsdsdk.auth — ELT 用户认证模块""" + +__all__ = [ + 'ELTAuthentication', + 'ELT_COOKIE_NAME', 'ELT_COOKIE_DOMAIN', 'ELT_COOKIE_MAX_AGE', 'SetELTCookie', 'DeleteELTCookie', + 'HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256', + 'ELTAuthMiddleware', 'CurrentRoleMiddleware', + 'IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'PermissionVerifier', + 'RoleManager', 'ShopManager', 'PositionManager', +] + + +def __getattr__(name): + if name == 'ELTAuthentication': + from gvsdsdk.auth.elt_auth import ELTAuthentication + globals()['ELTAuthentication'] = ELTAuthentication + return ELTAuthentication + if name in ('ELT_COOKIE_NAME', 'ELT_COOKIE_DOMAIN', 'ELT_COOKIE_MAX_AGE', 'SetELTCookie', 'DeleteELTCookie'): + from gvsdsdk.auth import cookie as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('HashPassword', 'VerifyPassword', 'SymmetricEncrypt', 'SymmetricDecrypt', 'FileSHA1', 'FileSHA256', 'BytesSHA1', 'BytesSHA256'): + from gvsdsdk.auth import crypto as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('ELTAuthMiddleware', 'CurrentRoleMiddleware'): + from gvsdsdk.auth import middleware as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('IsAdminUser', 'IsCompanyMember', 'CompanyScopedMixin', 'PermissionVerifier'): + from gvsdsdk.auth import permissions as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name == 'RoleManager': + from gvsdsdk.auth.managers import RoleManager + globals()['RoleManager'] = RoleManager + return RoleManager + if name == 'ShopManager': + from gvsdsdk.auth.managers import ShopManager + globals()['ShopManager'] = ShopManager + return ShopManager + if name == 'PositionManager': + from gvsdsdk.auth.managers import PositionManager + globals()['PositionManager'] = PositionManager + return PositionManager + raise AttributeError(f"module 'gvsdsdk.auth' has no attribute {name!r}") diff --git a/gvsdsdk/auth/cookie.py b/gvsdsdk/auth/cookie.py new file mode 100644 index 0000000..3f2c2bb --- /dev/null +++ b/gvsdsdk/auth/cookie.py @@ -0,0 +1,33 @@ +from django.conf import settings +from django.http import HttpResponse + +ELT_COOKIE_NAME = getattr(settings, 'ELT_COOKIE_NAME', 'ELToken') +ELT_COOKIE_DOMAIN = getattr(settings, 'ELT_COOKIE_DOMAIN', '.gvsds.com') +ELT_COOKIE_MAX_AGE = getattr(settings, 'ELT_COOKIE_MAX_AGE', 30 * 24 * 60 * 60) + + +def SetELTCookie(response, token_str): + response.set_cookie( + ELT_COOKIE_NAME, token_str, + domain=ELT_COOKIE_DOMAIN, + max_age=ELT_COOKIE_MAX_AGE, + secure=True, httponly=True, samesite='None', + ) + + +def DeleteELTCookie(response): + response.delete_cookie( + ELT_COOKIE_NAME, + domain=ELT_COOKIE_DOMAIN, + samesite='None', + ) + + +def GetDefaultAvatar(): + from io import BytesIO + from PIL import Image + img = Image.new('RGB', (256, 256), (200, 200, 200)) + buf = BytesIO() + img.save(buf, format='PNG') + buf.seek(0) + return HttpResponse(buf.read(), content_type='image/png') diff --git a/gvsdsdk/auth/crypto.py b/gvsdsdk/auth/crypto.py new file mode 100644 index 0000000..a909851 --- /dev/null +++ b/gvsdsdk/auth/crypto.py @@ -0,0 +1,93 @@ +import os +import bcrypt +import base64 +import hashlib + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives import hashes as _crypto_hashes + + +def HashPassword(password, rounds=12): + if isinstance(password, str): + password = password.encode('utf-8') + return bcrypt.hashpw(password, bcrypt.gensalt(rounds=rounds)) + + +def VerifyPassword(password, hashed): + if isinstance(password, str): + password = password.encode('utf-8') + if isinstance(hashed, str): + hashed = hashed.encode('utf-8') + return bcrypt.checkpw(password, hashed) + + +def _derive_key(master_secret): + """使用 HKDF-SHA256 从任意长度的主密钥派生 256-bit AES 密钥""" + if isinstance(master_secret, str): + master_secret = master_secret.encode('utf-8') + hkdf = HKDF( + algorithm=_crypto_hashes.SHA256(), + length=32, + salt=b'gvsds-crypto-salt', + info=b'aes-gcm-256', + ) + return hkdf.derive(master_secret) + + +def SymmetricEncrypt(plaintext, master_secret): + """使用 AES-256-GCM 加密 + + 输出格式: base64(nonce_12 + ciphertext + tag_16) + """ + if isinstance(plaintext, str): + plaintext = plaintext.encode('utf-8') + key = _derive_key(master_secret) + aesgcm = AESGCM(key) + nonce = os.urandom(12) + ciphertext_with_tag = aesgcm.encrypt(nonce, plaintext, None) + return base64.b64encode(nonce + ciphertext_with_tag).decode('ascii') + + +def SymmetricDecrypt(ciphertext_b64, master_secret): + """使用 AES-256-GCM 解密 + + 输入格式: base64(nonce_12 + ciphertext + tag_16) + """ + raw = base64.b64decode(ciphertext_b64) + nonce = raw[:12] + ciphertext_with_tag = raw[12:] + key = _derive_key(master_secret) + aesgcm = AESGCM(key) + plaintext = aesgcm.decrypt(nonce, ciphertext_with_tag, None) + return plaintext.decode('utf-8') + + +def FileSHA1(filepath): + sha1 = hashlib.sha1() + with open(filepath, 'rb') as f: + while True: + chunk = f.read(8192) + if not chunk: + break + sha1.update(chunk) + return sha1.hexdigest() + + +def FileSHA256(filepath): + sha256 = hashlib.sha256() + with open(filepath, 'rb') as f: + while True: + chunk = f.read(8192) + if not chunk: + break + sha256.update(chunk) + return sha256.hexdigest() + + +def BytesSHA1(data): + return hashlib.sha1(data).hexdigest() + + +def BytesSHA256(data): + return hashlib.sha256(data).hexdigest() diff --git a/gvsdsdk/auth/elt_auth.py b/gvsdsdk/auth/elt_auth.py new file mode 100644 index 0000000..0492bda --- /dev/null +++ b/gvsdsdk/auth/elt_auth.py @@ -0,0 +1,46 @@ +import uuid +from gvsdsdk.auth.cookie import ELT_COOKIE_NAME + + +class ELTAuthentication: + def authenticate(self, request): + elt_token = request.COOKIES.get(ELT_COOKIE_NAME) + if not elt_token: + return None + + try: + token_uuid = uuid.UUID(elt_token) + except (ValueError, AttributeError): + return None + + from gvsdsdk.models import User, EphemeralToken + import datetime + + try: + elt = EphemeralToken.objects.get( + ELTUUID=token_uuid, + ELTStatus=1, + ) + except EphemeralToken.DoesNotExist: + return None + + now_utc = datetime.datetime.now(datetime.timezone.utc) + if elt.ELTEndTime.replace(tzinfo=None) < now_utc.replace(tzinfo=None): + return None + + # 滑动续期:剩余时间不足7天时,延长30天 + remaining = elt.ELTEndTime.replace(tzinfo=None) - now_utc.replace(tzinfo=None) + if remaining < datetime.timedelta(days=7): + elt.ELTEndTime = now_utc + datetime.timedelta(days=30) + elt.save(update_fields=['ELTEndTime']) + + try: + user = User.objects.get(UserUUID=elt.ELTUserUUID) + except User.DoesNotExist: + return None + + request.elt = elt + return (user, elt_token) + + def authenticate_header(self, request): + return 'Bearer' diff --git a/gvsdsdk/auth/managers.py b/gvsdsdk/auth/managers.py new file mode 100644 index 0000000..e0780f7 --- /dev/null +++ b/gvsdsdk/auth/managers.py @@ -0,0 +1,265 @@ +import uuid +import datetime +from django.contrib.auth.models import BaseUserManager + +from gvsdsdk.models import Role, UserRole, RolePermission, Permission, Shop, RoleShop, Position, PositionTemplate, PositionTemplatePermission, UserDept + + +class UserManager(BaseUserManager): + """用户管理器——提供用户创建与超级用户创建。 + 方法名保留蛇形命名以兼容 Django 内部约定。""" + + def create_user(self, username, password=None, email='', **extra_fields): + """创建普通用户""" + if not username: + raise ValueError('The Username field must be set') + user = self.model( + UserUUID=uuid.uuid4().bytes, + UserName=username, + UserEmail=email or '', + **extra_fields + ) + if password: + user.SetPassword(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, password=None, **extra_fields): + """创建超级用户""" + extra_fields.setdefault('UserAccountLicense', 1) + extra_fields.setdefault('UserPositionStatus', 0) + extra_fields.setdefault('IsStaff', True) + extra_fields.setdefault('IsSuperuser', True) + return self.create_user(username, password, **extra_fields) + + +class RoleManager: + """角色管理器——提供角色创建、分配、权限管理""" + + @staticmethod + def CreateRole(Name, TenantUUID, AssignScope=0, AssignScopeUUID=None, RoleType=2): + """创建角色""" + role = Role( + RoleUUID=uuid.uuid4().bytes, + TenantUUID=TenantUUID, + RoleName=Name, + RoleType=RoleType, + RoleStatus=1, + AssignScope=AssignScope, + AssignScopeUUID=AssignScopeUUID, + RoleDesc='', + CreateTime=datetime.datetime.utcnow(), + ) + role.save() + return role + + @staticmethod + def AssignRole(UserUUID, RoleUUID, CompanyUUID): + """为用户分配角色""" + user_role = UserRole( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID=UserUUID, + RoleUUID=RoleUUID, + CompanyUUID=CompanyUUID, + CreateTime=datetime.datetime.utcnow(), + ) + user_role.save() + return user_role + + @staticmethod + def RemoveRole(UserUUID, RoleUUID, CompanyUUID=None): + """移除用户的指定角色""" + qs = UserRole.objects.filter(UserUUID=UserUUID, RoleUUID=RoleUUID) + if CompanyUUID is not None: + qs = qs.filter(CompanyUUID=CompanyUUID) + deleted, _ = qs.delete() + return deleted > 0 + + @staticmethod + def GetRolesForUser(UserUUID, CompanyUUID=None): + """获取用户的角色""" + qs = UserRole.objects.filter(UserUUID=UserUUID) + if CompanyUUID is not None: + qs = qs.filter(CompanyUUID=CompanyUUID) + role_uuids = qs.values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + @staticmethod + def GetUsersForRole(RoleUUID): + """获取拥有指定角色的所有用户""" + from gvsdsdk.models import User + user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True) + return User.objects.filter(UserUUID__in=user_uuids) + + @staticmethod + def AssignPermission(RoleUUID, PermUUID): + """为角色分配权限""" + rp = RolePermission( + RolePermUUID=uuid.uuid4().bytes, + RoleUUID=RoleUUID, + PermUUID=PermUUID, + CreateTime=datetime.datetime.utcnow(), + ) + rp.save() + return rp + + @staticmethod + def RemovePermission(RoleUUID, PermUUID): + """移除角色的指定权限""" + deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete() + return deleted > 0 + + @staticmethod + def GetPermissionsForRole(RoleUUID): + """获取角色的所有权限""" + perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + +class ShopManager: + """商铺管理器——提供商铺创建、角色关联、查询""" + + @staticmethod + def CreateShop(Name, CompanyUUID, Code='', ShopType=0, **Kwargs): + """创建商铺""" + shop = Shop( + ShopUUID=uuid.uuid4().bytes, + CompanyUUID=CompanyUUID, + ShopName=Name, + ShopCode=Code, + ShopType=ShopType, + ShopStatus=1, + ShopAddress=Kwargs.get('ShopAddress', ''), + ShopContact=Kwargs.get('ShopContact', ''), + ShopPhone=Kwargs.get('ShopPhone', ''), + ShopDesc=Kwargs.get('ShopDesc', ''), + CreateTime=datetime.datetime.utcnow(), + UpdateTime=datetime.datetime.utcnow(), + ) + shop.save() + return shop + + @staticmethod + def AssignRole(RoleUUID, ShopUUID, IsMainShop=False): + """为角色关联商铺""" + role_shop = RoleShop( + RoleShopUUID=uuid.uuid4().bytes, + RoleUUID=RoleUUID, + ShopUUID=ShopUUID, + IsMainShop=1 if IsMainShop else 0, + CreateTime=datetime.datetime.utcnow(), + ) + role_shop.save() + return role_shop + + @staticmethod + def RemoveRole(RoleUUID, ShopUUID): + """移除角色与商铺的关联""" + deleted, _ = RoleShop.objects.filter(RoleUUID=RoleUUID, ShopUUID=ShopUUID).delete() + return deleted > 0 + + @staticmethod + def GetShopsForRole(RoleUUID): + """获取角色关联的所有商铺""" + shop_uuids = RoleShop.objects.filter(RoleUUID=RoleUUID).values_list('ShopUUID', flat=True) + return Shop.objects.filter(ShopUUID__in=shop_uuids) + + @staticmethod + def GetRolesForShop(ShopUUID): + """获取商铺关联的所有角色""" + role_uuids = RoleShop.objects.filter(ShopUUID=ShopUUID).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + @staticmethod + def GetMainShopForRole(RoleUUID): + """获取角色的主商铺""" + rs = RoleShop.objects.filter(RoleUUID=RoleUUID, IsMainShop=1).first() + if rs: + return Shop.objects.filter(ShopUUID=rs.ShopUUID).first() + return None + + @staticmethod + def GetShopsForCompany(CompanyUUID): + """获取公司下的所有商铺""" + return Shop.objects.filter(CompanyUUID=CompanyUUID, ShopStatus=1) + + +class PositionManager: + """职务管理器——提供人事职务标签创建、岗位模板管理""" + + @staticmethod + def CreatePosition(Name, TenantUUID, Code='', Level=0, **Kwargs): + """创建人事职务标签""" + position = Position( + PositionUUID=uuid.uuid4().bytes, + TenantUUID=TenantUUID, + PositionName=Name, + PositionCode=Code, + PositionLevel=Level, + PositionStatus=1, + PositionDesc=Kwargs.get('PositionDesc', ''), + CreateTime=datetime.datetime.utcnow(), + UpdateTime=datetime.datetime.utcnow(), + ) + position.save() + return position + + @staticmethod + def GetPositionsForTenant(TenantUUID): + """获取租户下的所有职务""" + return Position.objects.filter(TenantUUID=TenantUUID, PositionStatus=1) + + @staticmethod + def CreatePositionTemplate(Name, TenantUUID, Code='', **Kwargs): + """创建岗位模板""" + template = PositionTemplate( + PositionTemplateUUID=uuid.uuid4().bytes, + TenantUUID=TenantUUID, + TemplateName=Name, + TemplateCode=Code, + TemplateStatus=1, + TemplateDesc=Kwargs.get('TemplateDesc', ''), + CreateTime=datetime.datetime.utcnow(), + UpdateTime=datetime.datetime.utcnow(), + ) + template.save() + return template + + @staticmethod + def AssignPermToTemplate(TemplateUUID, PermUUID): + """为岗位模板关联权限""" + ptp = PositionTemplatePermission( + PTPermUUID=uuid.uuid4().bytes, + PositionTemplateUUID=TemplateUUID, + PermUUID=PermUUID, + CreateTime=datetime.datetime.utcnow(), + ) + ptp.save() + return ptp + + @staticmethod + def RemovePermFromTemplate(TemplateUUID, PermUUID): + """移除岗位模板与权限的关联""" + deleted, _ = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=TemplateUUID, PermUUID=PermUUID + ).delete() + return deleted > 0 + + @staticmethod + def GetPermsForTemplate(TemplateUUID): + """获取岗位模板关联的所有权限""" + perm_uuids = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=TemplateUUID + ).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + @staticmethod + def GetTemplatesForTenant(TenantUUID): + """获取租户下的所有岗位模板""" + return PositionTemplate.objects.filter(TenantUUID=TenantUUID, TemplateStatus=1) + + @staticmethod + def CopyTemplateToRole(TemplateUUID, RoleName, TenantUUID): + """从岗位模板复制权限到新角色""" + template = PositionTemplate.objects.get(PositionTemplateUUID=TemplateUUID) + return template.CopyToRole(RoleName, TenantUUID) diff --git a/gvsdsdk/auth/middleware.py b/gvsdsdk/auth/middleware.py new file mode 100644 index 0000000..bf92140 --- /dev/null +++ b/gvsdsdk/auth/middleware.py @@ -0,0 +1,54 @@ +from gvsdsdk.auth.cookie import ELT_COOKIE_NAME +from gvsdsdk.models import UserRole, Role +from gvsdsdk.model_utils import UUIDObj + + +class ELTAuthMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + elt_token = request.COOKIES.get(ELT_COOKIE_NAME) + if elt_token: + request.elt_token = elt_token + response = self.get_response(request) + return response + + +class CurrentRoleMiddleware: + """解析 CurrentRoleUUID Cookie,设置 request.current_role 和 request.current_company_uuid + + request.current_role : gvsdsdk.models.Role 实例(或 None) + request.current_company_uuid : 通过 UserRole.CompanyUUID 获取当前角色所属公司UUID + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + request.current_role = None + request.current_company_uuid = None + + # 仅对已认证用户处理角色 + if hasattr(request, 'user') and request.user and request.user.is_authenticated: + role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '') + if role_uuid_str: + try: + role_uuid_bytes = UUIDObj(role_uuid_str).bytes + # gvsdsdk.UserRole 的 RoleUUID 是 UUIDField,不能用 select_related + user_role = UserRole.objects.filter( + UserUUID=request.user.UserUUID, + RoleUUID=role_uuid_bytes + ).first() + if user_role: + # 单独查询 Role 实例 + role = Role.objects.filter(RoleUUID=role_uuid_bytes).first() + if role: + request.current_role = role + # 通过 UserRole.CompanyUUID 获取当前公司 + if user_role.CompanyUUID: + request.current_company_uuid = user_role.CompanyUUID + except Exception: + pass + + return self.get_response(request) diff --git a/gvsdsdk/auth/permissions.py b/gvsdsdk/auth/permissions.py new file mode 100644 index 0000000..44543d4 --- /dev/null +++ b/gvsdsdk/auth/permissions.py @@ -0,0 +1,109 @@ +from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC +from rest_framework.permissions import BasePermission + + +class IsAdminUser(BasePermission): + """仅允许管理员(IsStaff 或 IsSuperuser)访问""" + + def has_permission(self, request, view): + user = request.user + if not user or not user.is_authenticated: + return False + if not isinstance(user, User): + return False + return user.IsStaff or user.IsSuperuser + + +class IsCompanyMember(BasePermission): + """验证用户当前角色属于目标数据所在公司""" + def has_object_permission(self, request, view, obj): + if not request.current_company_uuid: + return False + obj_company = getattr(obj, 'CompanyUUID_id', None) or getattr(obj, 'CompanyUUID', None) + if obj_company is None: + return True # 无公司归属的数据不做限制 + return str(obj_company) == str(request.current_company_uuid) + + +class CompanyScopedMixin: + """ViewSet Mixin:自动按当前角色所属公司过滤 queryset""" + company_field = 'CompanyUUID' # 可在 ViewSet 中覆盖 + + def get_queryset(self): + qs = super().get_queryset() + company_uuid = getattr(self.request, 'current_company_uuid', None) + if company_uuid and hasattr(qs.model, self.company_field): + qs = qs.filter(**{self.company_field: company_uuid}) + return qs + + def perform_create(self, serializer): + company_uuid = getattr(self.request, 'current_company_uuid', None) + if company_uuid and hasattr(serializer.Meta.model, 'CompanyUUID'): + serializer.save(CompanyUUID=company_uuid) + else: + serializer.save() + + +def PermissionVerifier(permission, attributes, user=None, request=None): + """权限验证器——混合 ABAC + RBAC 检查 + + RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限, + 通过 RolePermission → Permission 链路查询。 + 超级管理员/员工直接放行。 + """ + if not permission: + return True + if user is None: + return False + if not isinstance(user, User): + return False + + # 超级管理员和员工直接放行 + if user.IsSuperuser or user.IsStaff: + return True + + current_role = getattr(request, 'current_role', None) if request else None + + for attr in attributes: + # ABAC 检查 + abac_match = UserABAC.objects.filter( + UserUUID=user.UserUUID, + HierarchicalPermissionIdentifiers=attr + ).first() + if abac_match: + continue + + # RBAC 检查:仅检查当前角色 + if current_role and isinstance(current_role, Role): + if current_role.RoleStatus != 1: + return False + perm_uuids = RolePermission.objects.filter( + RoleUUID=current_role.RoleUUID + ).values_list('PermUUID', flat=True) + rbac_result = Permission.objects.filter( + PermUUID__in=perm_uuids, + PermCode=attr, + PermStatus=1, + ).exists() + else: + # 无 current_role 时回退到用户所有活跃角色 + user_role_uuids = UserRole.objects.filter( + UserUUID=user.UserUUID + ).values_list('RoleUUID', flat=True) + active_role_uuids = Role.objects.filter( + RoleUUID__in=user_role_uuids, + RoleStatus=1, + ).values_list('RoleUUID', flat=True) + perm_uuids = RolePermission.objects.filter( + RoleUUID__in=active_role_uuids + ).values_list('PermUUID', flat=True) + rbac_result = Permission.objects.filter( + PermUUID__in=perm_uuids, + PermCode=attr, + PermStatus=1, + ).exists() + + if rbac_result: + continue + return False + return True diff --git a/gvsdsdk/commission/__init__.py b/gvsdsdk/commission/__init__.py new file mode 100644 index 0000000..d945306 --- /dev/null +++ b/gvsdsdk/commission/__init__.py @@ -0,0 +1,28 @@ +"""gvsdsdk.commission — 分佣引擎""" + +__all__ = [ + 'CommissionEvent', 'RateTable', 'RateEntry', 'CommissionRule', + 'CommissionRecord', 'PreSettlement', + 'CommissionEngine', 'CommissionError', + 'CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy', + 'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy', +] + + +def __getattr__(name): + if name in ('CommissionEvent', 'RateTable', 'RateEntry', 'CommissionRule', 'CommissionRecord', 'PreSettlement'): + from gvsdsdk.commission import models as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('CommissionEngine', 'CommissionError'): + from gvsdsdk.commission import manager as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy', 'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy'): + from gvsdsdk.commission.strategies import engine as _m + val = getattr(_m, name) + globals()[name] = val + return val + raise AttributeError(f"module 'gvsdsdk.commission' has no attribute {name!r}") diff --git a/gvsdsdk/commission/manager.py b/gvsdsdk/commission/manager.py new file mode 100644 index 0000000..3bd0b42 --- /dev/null +++ b/gvsdsdk/commission/manager.py @@ -0,0 +1,519 @@ +"""模块:分佣编排器 +SAAS 级分佣引擎核心入口——子服务通过此引擎触发分佣事件。 + +职责: + 1. 接收分佣事件(Dispatch) + 2. 匹配分佣规则 + 3. 执行分账策略计算 + 4. 写入 CommissionRecord 流水 + 5. 支持预结算(跨平台确认) + 6. 支持邀请链分佣(多级分佣)""" + +import uuid +import datetime +import logging +from decimal import Decimal +from typing import List, Optional, Dict, Any +from django.db.models import Sum as _AggSum +from gvsdsdk.commission.models import ( + CommissionEvent, + CommissionRule, + CommissionRecord, + RateTable, + PreSettlement, +) +from gvsdsdk.commission.strategies.engine import ( + CreateStrategy, + RemainderStrategy, +) + +logger = logging.getLogger(__name__) + + +class CommissionError(Exception): + def __init__(self, message, event=None): + super().__init__(message) + self.Event = event + + +class CommissionEngine: + """分佣编排器——SAAS 主服务器核心入口 + + 用法: + engine = CommissionEngine(tenant_uuid='xxx') + + # 配置分佣规则(SAAS管理员操作) + engine.ConfigureRule( + event_type='order_complete', + rule_name='平台订单打手8成', + participant_rules=[ + {'role': 'worker', 'type': 'percentage', 'value': 0.80}, + {'role': 'platform', 'type': 'remainder'}, + ], + ) + + # 子服务触发分佣 + records = engine.Dispatch( + event_type='order_complete', + event_key='ORDER-20260611-001', + total_amount=Decimal('100.00'), + source_user_uuid='boss_001', + participants={ + 'worker': 'dashou_123', + 'agent': 'guanshi_456', + 'leader': 'zuzhang_789', + }, + tier_index=1, + payload={'order_id': 'xxx', 'game': 'LoL'}, + ) + + # 跨平台预结算 + engine.PreSettle( + event_key='ORDER-CROSS-001', + external_platform='partner_club_002', + external_order_id='P-ORDER-888', + participant_id='dashou_123', + amount=Decimal('80.00'), + order_amount=Decimal('100.00'), + ) + """ + + def __init__(self, tenant_uuid): + self.TenantUUID = tenant_uuid + + def ConfigureRule(self, event_type, rule_name, participant_rules, + rule_description='', max_invite_level=2, + rate_table_uuid=None, rule_priority=0): + """配置分佣规则——SAAS 管理员为租户配置分佣策略 + + Args: + event_type: 事件类型 ('order_complete', 'member_purchase', ...) + rule_name: 规则名称 + participant_rules: 参与者分账规则列表 + [ + {'role': 'worker', 'type': 'percentage', 'value': 0.80}, + {'role': 'agent', 'type': 'fixed', 'value': 10}, + {'role': 'leader', 'type': 'tiered', 'tiers': {1:5, 2:8, 3:10}}, + {'role': 'platform', 'type': 'remainder'}, + ] + rule_description: 规则描述 + max_invite_level: 邀请链最大层级 + rate_table_uuid: 关联费率表UUID + rule_priority: 优先级(数字越大越优先) + + Returns: + CommissionRule 实例 + """ + rate_table = None + if rate_table_uuid: + try: + rate_table = RateTable.objects.get( + RateTableUUID=rate_table_uuid, TenantUUID=self.TenantUUID, + ) + except RateTable.DoesNotExist: + raise CommissionError(f'费率表不存在: {rate_table_uuid}') + + rule = CommissionRule.objects.create( + RuleUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + EventType=event_type, + RuleName=rule_name, + RuleDescription=rule_description, + ParticipantRules=participant_rules, + RateTable=rate_table, + MaxInviteLevel=max_invite_level, + RulePriority=rule_priority, + ) + logger.info(f'分佣规则已配置: {rule_name}@tenant={self.TenantUUID}') + return rule + + def ConfigureRateTable(self, table_code, table_name, entries, + table_description=''): + """配置费率表 + + Args: + table_code: 表编码(如 'dashou_order_split') + table_name: 表名 + entries: 费率条目字典 {key: value} + table_description: 描述 + + Returns: + RateTable 实例 + """ + from gvsdsdk.commission.models import RateEntry + + table = RateTable.objects.create( + RateTableUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + TableCode=table_code, + TableName=table_name, + TableDescription=table_description, + ) + + for key, value in entries.items(): + RateEntry.objects.create( + EntryUUID=uuid.uuid4().bytes, + RateTable=table, + RateKey=str(key), + RateValue=Decimal(str(value)), + ) + + logger.info(f'费率表已创建: {table_code}@tenant={self.TenantUUID}') + return table + + def Dispatch(self, event_type, event_key, total_amount, + source_user_uuid=None, participants=None, + tier_index=0, payload=None, auto_settle=True): + """触发分佣事件——子服务调用的核心入口 + + Args: + event_type: 事件类型 + event_key: 业务唯一键(幂等保护) + total_amount: 可分佣总金额 + source_user_uuid: 触发者UUID(如付费用户) + participants: 参与者映射 {role: user_id} + tier_index: 阶梯索引(多次分红时用,第1次=1) + payload: 业务上下文(JSON) + auto_settle: 是否自动结算 + + Returns: + List[CommissionRecord] 分佣记录列表 + + Raises: + CommissionError: 重复事件或规则未配置 + """ + if CommissionEvent.objects.filter( + TenantUUID=self.TenantUUID, EventKey=event_key, + ).exists(): + raise CommissionError(f'事件已处理: {event_key}') + + total_amount = Decimal(str(total_amount)) + + event = CommissionEvent.objects.create( + EventUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + EventType=event_type, + EventKey=event_key, + TotalAmount=total_amount, + SourceUserUUID=source_user_uuid, + EventPayload=payload or {}, + Participants=participants or {}, + EventStatus=CommissionEvent.EVT_PENDING, + ) + + rules = CommissionRule.objects.filter( + TenantUUID=self.TenantUUID, + EventType=event_type, + RuleStatus=1, + ).order_by('-RulePriority') + + if not rules.exists(): + event.MarkFailed() + raise CommissionError(f'未配置分佣规则: {event_type}', event=event) + + event.MarkProcessing() + + all_records = [] + for rule in rules: + records = self._execute_rule( + event=event, + rule=rule, + total_amount=total_amount, + participants=participants or {}, + tier_index=tier_index, + ) + all_records.extend(records) + + if auto_settle: + event.MarkSettled() + for rec in all_records: + rec.Settle() + + return all_records + + def _execute_rule(self, event, rule, total_amount, participants, tier_index): + """执行单条分佣规则""" + rate_table = rule.RateTable + participant_rules = rule.ParticipantRules + records = [] + allocated = Decimal('0') + + remainder_rule = None + ordered_rules = sorted( + participant_rules, + key=lambda r: 0 if r.get('Type') == 'remainder' else 1, + ) + + for pr in ordered_rules: + if pr.get('Type') == 'remainder': + remainder_rule = pr + continue + + strategy = CreateStrategy(pr, rate_table) + amount = strategy.Calculate( + total_amount, + tier_index=tier_index, + ) + allocated += amount + + participant_id = self._resolve_participant(pr, participants) + if amount > 0: + records.append(self._create_record( + event=event, + rule=rule, + participant_id=participant_id, + role=pr.get('Role', 'unknown'), + split_type=pr.get('Type', 'unknown'), + amount=amount, + rule_snapshot=pr, + )) + + if remainder_rule and allocated < total_amount: + strategy = CreateStrategy(remainder_rule, rate_table) + amount = strategy.Calculate( + total_amount, + allocated_amount=allocated, + ) + participant_id = self._resolve_participant(remainder_rule, participants) + if amount > 0: + records.append(self._create_record( + event=event, + rule=rule, + participant_id=participant_id, + role=remainder_rule.get('role', 'platform'), + split_type='remainder', + amount=amount, + rule_snapshot=remainder_rule, + )) + + for rec in records: + rec.TierIndex = tier_index + + return records + + def _resolve_participant(self, participant_rule, participants): + role = participant_rule.get('Role', 'unknown') + Fallback = participant_rule.get('FallbackID', '') + return participants.get(role, Fallback or f'{role}_default') + + def _create_record(self, event, rule, participant_id, role, split_type, amount, rule_snapshot): + return CommissionRecord.objects.create( + RecordUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + Event=event, + Rule=rule, + ParticipantID=str(participant_id), + ParticipantRole=role, + SplitType=split_type, + Amount=amount, + RuleSnapshot=rule_snapshot, + ) + + def PreSettle(self, event_key, external_platform, external_order_id, + participant_id, amount, order_amount=Decimal('0'), + participant_role='worker', confirm_deadline=None): + """创建预结算记录——跨平台订单待确认 + + Args: + event_key: 业务唯一键 + external_platform: 外部平台标识 + external_order_id: 外部订单ID + participant_id: 应分账对象 + amount: 预分金额 + order_amount: 订单原金额 + participant_role: 参与者角色 + confirm_deadline: 确认截止时间 + + Returns: + PreSettlement 实例 + """ + ps = PreSettlement.objects.create( + SettlementUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + EventKey=event_key, + ExternalPlatform=external_platform, + ExternalOrderID=external_order_id, + ParticipantID=participant_id, + ParticipantRole=participant_role, + Amount=Decimal(str(amount)), + OrderAmount=Decimal(str(order_amount)), + ConfirmDeadline=confirm_deadline, + ) + logger.info( + f'预结算: {event_key}->{participant_id}={amount}' + f'@tenant={self.TenantUUID}' + ) + return ps + + def ConfirmPreSettlement(self, event_key, participant_id=None, + confirmed_by=''): + """确认预结算——转为正式分佣记录 + + Args: + event_key: 业务唯一键 + participant_id: 参与者ID(None=全部确认) + confirmed_by: 确认人 + + Returns: + int 确认条数 + """ + qs = PreSettlement.objects.filter( + TenantUUID=self.TenantUUID, + EventKey=event_key, + Status=PreSettlement.PST_PENDING, + ) + if participant_id: + qs = qs.filter(ParticipantID=participant_id) + + count = 0 + for ps in qs: + ps.Confirm(confirmed_by) + count += 1 + return count + + def RejectPreSettlement(self, event_key, participant_id=None, + confirmed_by=''): + """拒绝预结算 + + Args: + event_key: 业务唯一键 + participant_id: 参与者ID(None=全部拒绝) + confirmed_by: 操作人 + + Returns: + int 拒绝条数 + """ + qs = PreSettlement.objects.filter( + TenantUUID=self.TenantUUID, + EventKey=event_key, + Status=PreSettlement.PST_PENDING, + ) + if participant_id: + qs = qs.filter(ParticipantID=participant_id) + + count = 0 + for ps in qs: + ps.Reject(confirmed_by) + count += 1 + return count + + def CalculateFee(self, total_amount, rate_table_code, rate_key): + """计算手续费——用于提现场景 + + Args: + total_amount: 提现金额 + rate_table_code: 费率表编码 + rate_key: 费率键 + + Returns: + Decimal 手续费金额 + """ + total_amount = Decimal(str(total_amount)) + try: + table = RateTable.objects.get( + TenantUUID=self.TenantUUID, + TableCode=rate_table_code, + TableStatus=1, + ) + except RateTable.DoesNotExist: + return Decimal('0') + + rate = table.GetRate(rate_key) + if rate is None: + return Decimal('0') + + return total_amount * rate + + def QueryRecords(self, participant_id=None, event_type=None, + status=None, start_date=None, end_date=None, + limit=50): + """查询分佣记录 + + Args: + participant_id: 参与者ID + event_type: 事件类型 + status: 状态 + start_date: 开始日期 + end_date: 结束日期 + limit: 返回条数 + + Returns: + QuerySet[CommissionRecord] + """ + qs = CommissionRecord.objects.filter(TenantUUID=self.TenantUUID) + + if participant_id: + qs = qs.filter(ParticipantID=participant_id) + if status is not None: + qs = qs.filter(Status=status) + if event_type: + qs = qs.filter(Event__EventType=event_type) + if start_date: + qs = qs.filter(CreateTime__gte=start_date) + if end_date: + qs = qs.filter(CreateTime__lte=end_date) + + return qs.order_by('-CreateTime')[:limit] + + def TotalByParticipant(self, participant_id, start_date=None, end_date=None): + """统计某参与者的分佣总额""" + qs = CommissionRecord.objects.filter( + TenantUUID=self.TenantUUID, + ParticipantID=participant_id, + Status__gte=CommissionRecord.REC_SETTLED, + ) + if start_date: + qs = qs.filter(CreateTime__gte=start_date) + if end_date: + qs = qs.filter(CreateTime__lte=end_date) + + result = qs.aggregate(total=_AggSum('Amount')) + return result['total'] or Decimal('0') + + def BuildInviteChainCommission(self, event_type, event_key, total_amount, + source_user_uuid, invite_chain, + rule_name, participant_rules): + """邀请链分佣——按层级对受邀人行为进行多级分佣 + + Args: + event_type: 事件类型 + event_key: 事件键 + total_amount: 总金额 + source_user_uuid: 行为触发者 + invite_chain: 邀请链 [{user_uuid, level, role}, ...] + level=1 为直接邀请人,level=2 为间推 + rule_name: 规则名称 + participant_rules: 参与者规则(按层级配置) + 例: [{'role': 'agent_l1', 'type': 'fixed', 'value': 10}, + {'role': 'agent_l2', 'type': 'fixed', 'value': 5}] + + Returns: + List[CommissionRecord] + """ + rule = CommissionRule.objects.filter( + TenantUUID=self.TenantUUID, + EventType=event_type, + RuleName=rule_name, + RuleStatus=1, + ).first() + + if not rule: + rule = self.ConfigureRule( + event_type=event_type, + rule_name=rule_name, + participant_rules=participant_rules, + ) + + participants = { + item.get('Role', f'invite_l{item.get("Level",0)}'): item.get('UserUUID', '') + for item in invite_chain + } + participants['source'] = source_user_uuid + + return self.Dispatch( + event_type=event_type, + event_key=event_key, + total_amount=total_amount, + source_user_uuid=source_user_uuid, + participants=participants, + ) diff --git a/gvsdsdk/commission/models.py b/gvsdsdk/commission/models.py new file mode 100644 index 0000000..bbffd85 --- /dev/null +++ b/gvsdsdk/commission/models.py @@ -0,0 +1,352 @@ +"""模块:分佣 SAAS 数据模型 +SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。 +子服务只管说"发生了什么",SAAS 负责"怎么分"。""" + +import uuid +import datetime +from django.db import models +from gvsdsdk.model_base import QModel +from gvsdsdk.models import UUIDField + + +class CommissionEvent(QModel): + """分佣事件表——子服务触发的分佣事件 + + 子服务在业务发生时(订单完成、会员购买、充值到账等) + 调用 CommissionEngine.Dispatch() 写入此表。 + """ + + EVT_ORDER_COMPLETE = 'order_complete' + EVT_MEMBER_PURCHASE = 'member_purchase' + EVT_RECHARGE = 'recharge' + EVT_PENALTY = 'penalty' + EVT_REGISTER = 'register' + EVT_CUSTOM = 'custom' + + EVT_TYPE_CHOICES = [ + (EVT_ORDER_COMPLETE, '订单完成'), + (EVT_MEMBER_PURCHASE, '会员购买'), + (EVT_RECHARGE, '充值到账'), + (EVT_PENALTY, '罚款缴纳'), + (EVT_REGISTER, '用户注册'), + (EVT_CUSTOM, '自定义事件'), + ] + + EVT_PENDING = 0 + EVT_PROCESSING = 1 + EVT_SETTLED = 2 + EVT_FAILED = 3 + + EVT_STATUS_CHOICES = [ + (EVT_PENDING, '待处理'), + (EVT_PROCESSING, '处理中'), + (EVT_SETTLED, '已结算'), + (EVT_FAILED, '结算失败'), + ] + + EventUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES) + EventKey = models.CharField(max_length=128) + TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0) + SourceUserUUID = UUIDField(null=True, blank=True) + EventPayload = models.JSONField(default=dict) + Participants = models.JSONField(default=dict) + EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES) + EventTime = models.DateTimeField(default=datetime.datetime.utcnow) + ProcessTime = models.DateTimeField(null=True, blank=True) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + + class Meta: + db_table = 'CommissionEvent' + app_label = 'gvsdsdk' + verbose_name = '分佣事件' + verbose_name_plural = '分佣事件' + unique_together = [('TenantUUID', 'EventKey')] + managed = False + indexes = [ + models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'), + models.Index(fields=['EventKey'], name='idx_cmevt_event_key'), + models.Index(fields=['CreateTime'], name='idx_cmevt_create_time'), + ] + + def __str__(self): + return f'{self.EventType}:{self.EventKey}' + + @property + def IsSettled(self): + return self.EventStatus == self.EVT_SETTLED + + def MarkProcessing(self): + self.EventStatus = self.EVT_PROCESSING + self.save(update_fields=['EventStatus']) + + def MarkSettled(self): + self.EventStatus = self.EVT_SETTLED + self.ProcessTime = datetime.datetime.utcnow() + self.save(update_fields=['EventStatus', 'ProcessTime']) + + def MarkFailed(self): + self.EventStatus = self.EVT_FAILED + self.ProcessTime = datetime.datetime.utcnow() + self.save(update_fields=['EventStatus', 'ProcessTime']) + + +class RateTable(QModel): + """费率表——可复用的费率配置 + + 每个租户可创建多张费率表,不同事件类型绑定不同的表。 + 例如:打手分成费率表、提现手续费率表、罚款分红费率表。 + """ + + RateTableUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + TableCode = models.CharField(max_length=64) + TableName = models.CharField(max_length=128) + TableDescription = models.CharField(max_length=512, default='') + TableStatus = models.SmallIntegerField(default=1) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + UpdateTime = models.DateTimeField(null=True, blank=True) + + class Meta: + db_table = 'CommissionRateTable' + app_label = 'gvsdsdk' + verbose_name = '费率表' + verbose_name_plural = '费率表' + unique_together = [('TenantUUID', 'TableCode')] + managed = False + indexes = [ + models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'), + ] + + def __str__(self): + return f'{self.TableCode}:{self.TableName}' + + def GetRate(self, key): + try: + entry = self.Entries.get(RateKey=key, EntryStatus=1) + return entry.RateValue + except RateEntry.DoesNotExist: + return None + + def GetRates(self): + return { + e.RateKey: e.RateValue + for e in self.Entries.filter(EntryStatus=1) + } + + +class RateEntry(QModel): + """费率条目——费率表中的一行""" + + EntryUUID = UUIDField(primary_key=True) + RateTable = models.ForeignKey( + RateTable, on_delete=models.CASCADE, + related_name='Entries', + ) + RateKey = models.CharField(max_length=64) + RateLabel = models.CharField(max_length=128, default='') + RateValue = models.DecimalField(max_digits=10, decimal_places=6) + EntryStatus = models.SmallIntegerField(default=1) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + + class Meta: + db_table = 'CommissionRateEntry' + app_label = 'gvsdsdk' + verbose_name = '费率条目' + verbose_name_plural = '费率条目' + unique_together = [('RateTable', 'RateKey')] + managed = False + indexes = [ + models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'), + ] + + def __str__(self): + return f'{self.RateKey}={self.RateValue}' + + +class CommissionRule(QModel): + """分佣规则表——定义"怎么分" + + 每条规则绑定一种事件类型,定义参与者如何分账。 + 支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。 + """ + + SPLT_PERCENTAGE = 'percentage' + SPLT_FIXED = 'fixed' + SPLT_TIERED = 'tiered' + SPLT_REMAINDER = 'remainder' + + SPLT_TYPE_CHOICES = [ + (SPLT_PERCENTAGE, '比例分账'), + (SPLT_FIXED, '固定金额'), + (SPLT_TIERED, '阶梯分账'), + (SPLT_REMAINDER, '剩余归平台'), + ] + + RuleUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES) + RuleName = models.CharField(max_length=128) + RuleDescription = models.CharField(max_length=512, default='') + ParticipantRules = models.JSONField(default=list) + RateTable = models.ForeignKey( + RateTable, on_delete=models.SET_NULL, + null=True, blank=True, related_name='rules', + ) + MaxInviteLevel = models.SmallIntegerField(default=2) + RulePriority = models.IntegerField(default=0) + RuleStatus = models.SmallIntegerField(default=1) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + UpdateTime = models.DateTimeField(null=True, blank=True) + + class Meta: + db_table = 'CommissionRule' + app_label = 'gvsdsdk' + verbose_name = '分佣规则' + verbose_name_plural = '分佣规则' + managed = False + indexes = [ + models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'), + models.Index(fields=['RulePriority'], name='idx_cmrule_priority'), + ] + + def __str__(self): + return f'{self.RuleName}({self.EventType})' + + def FindParticipantRule(self, role): + for pr in self.ParticipantRules: + if pr.get('role') == role: + return pr + return None + + +class CommissionRecord(QModel): + """分佣记录表——不可变分账流水 + + 每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。 + 生成后即冻结,规则变更不影响历史记录。 + """ + + REC_PENDING = 0 + REC_SETTLED = 1 + REC_WITHDRAWN = 2 + REC_CANCELLED = 3 + + REC_STATUS_CHOICES = [ + (REC_PENDING, '待结算'), + (REC_SETTLED, '已结算'), + (REC_WITHDRAWN, '已提现'), + (REC_CANCELLED, '已取消'), + ] + + RecordUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + Event = models.ForeignKey( + CommissionEvent, on_delete=models.CASCADE, + related_name='records', + ) + Rule = models.ForeignKey( + CommissionRule, on_delete=models.SET_NULL, + null=True, related_name='records', + ) + ParticipantID = models.CharField(max_length=128) + ParticipantRole = models.CharField(max_length=64) + SplitType = models.CharField(max_length=16) + Amount = models.DecimalField(max_digits=18, decimal_places=4) + RuleSnapshot = models.JSONField(default=dict) + Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES) + TierIndex = models.IntegerField(default=0) + SettleTime = models.DateTimeField(null=True, blank=True) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + + class Meta: + db_table = 'CommissionRecord' + app_label = 'gvsdsdk' + verbose_name = '分佣记录' + verbose_name_plural = '分佣记录' + ordering = ['-CreateTime'] + managed = False + indexes = [ + models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'), + models.Index(fields=['Event'], name='idx_cmrec_event'), + models.Index(fields=['CreateTime'], name='idx_cmrec_create_time'), + ] + + def __str__(self): + return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}' + + @property + def IsSettled(self): + return self.Status >= self.REC_SETTLED + + def Settle(self): + self.Status = self.REC_SETTLED + self.SettleTime = datetime.datetime.utcnow() + self.save(update_fields=['Status', 'SettleTime']) + + def Cancel(self): + self.Status = self.REC_CANCELLED + self.save(update_fields=['Status']) + + +class PreSettlement(QModel): + """预结算表——跨平台订单待确认款项 + + 当订单涉及外部平台时,打手分成先挂预结算, + 对方确认后转为正式 CommissionRecord。 + """ + + PST_PENDING = 0 + PST_CONFIRMED = 1 + PST_REJECTED = 2 + + PST_STATUS_CHOICES = [ + (PST_PENDING, '待确认'), + (PST_CONFIRMED, '已确认'), + (PST_REJECTED, '已拒绝'), + ] + + SettlementUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + EventKey = models.CharField(max_length=128) + ExternalPlatform = models.CharField(max_length=64, default='') + ExternalOrderID = models.CharField(max_length=128, default='') + ParticipantID = models.CharField(max_length=128) + ParticipantRole = models.CharField(max_length=64, default='worker') + Amount = models.DecimalField(max_digits=18, decimal_places=4) + OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0) + Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES) + ConfirmedBy = models.CharField(max_length=128, default='') + ConfirmDeadline = models.DateTimeField(null=True, blank=True) + ConfirmedAt = models.DateTimeField(null=True, blank=True) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + UpdateTime = models.DateTimeField(null=True, blank=True) + + class Meta: + db_table = 'CommissionPreSettlement' + app_label = 'gvsdsdk' + verbose_name = '预结算' + verbose_name_plural = '预结算' + unique_together = [('EventKey', 'ParticipantID')] + managed = False + indexes = [ + models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'), + models.Index(fields=['EventKey'], name='idx_cmpst_event_key'), + models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'), + ] + + def __str__(self): + return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}' + + def Confirm(self, confirmed_by=''): + self.Status = self.PST_CONFIRMED + self.ConfirmedBy = confirmed_by + self.ConfirmedAt = datetime.datetime.utcnow() + self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt']) + + def Reject(self, confirmed_by=''): + self.Status = self.PST_REJECTED + self.ConfirmedBy = confirmed_by + self.ConfirmedAt = datetime.datetime.utcnow() + self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt']) diff --git a/gvsdsdk/commission/strategies/__init__.py b/gvsdsdk/commission/strategies/__init__.py new file mode 100644 index 0000000..4d57262 --- /dev/null +++ b/gvsdsdk/commission/strategies/__init__.py @@ -0,0 +1,10 @@ +"""gvsdsdk.commission.strategies — 分佣策略引擎""" + + +def __getattr__(name): + if name in ('CreateStrategy', 'BaseSplitStrategy', 'PercentageStrategy', 'FixedStrategy', 'TieredStrategy', 'RemainderStrategy', 'WithdrawalFeeStrategy'): + from gvsdsdk.commission.strategies import engine as _m + val = getattr(_m, name) + globals()[name] = val + return val + raise AttributeError(f"module 'gvsdsdk.commission.strategies' has no attribute {name!r}") diff --git a/gvsdsdk/commission/strategies/engine.py b/gvsdsdk/commission/strategies/engine.py new file mode 100644 index 0000000..11f0b06 --- /dev/null +++ b/gvsdsdk/commission/strategies/engine.py @@ -0,0 +1,156 @@ +"""模块:分佣策略引擎 +策略模式实现——每种分账方式独立封装,新增分账逻辑只需添加新策略类。 + +支持四种策略: + PercentageStrategy: 比例分账 → TotalAmount × rate + FixedStrategy: 固定金额 → 固定值(受保底/封顶约束) + TieredStrategy: 阶梯分账 → 按次数/档位查表 + RemainderStrategy: 剩余归平台 → TotalAmount - SUM(其他参与者) +""" + +from abc import ABC, abstractmethod +from decimal import Decimal +from typing import Dict, List, Optional +from gvsdsdk.commission.models import RateTable + + +class BaseSplitStrategy(ABC): + """分账策略抽象基类""" + + def __init__(self, participant_rule: dict, rate_table: Optional[RateTable] = None): + self._rule = participant_rule + self._rate_table = rate_table + + @abstractmethod + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + ... + + @property + def Role(self): + return self._rule.get('Role', 'unknown') + + @property + def SplitType(self): + return self._rule.get('Type', 'unknown') + + def _get_rate_from_table(self, key): + if self._rate_table: + rate = self._rate_table.GetRate(key) + if rate is not None: + return rate + return self._rule.get('Rate', Decimal('0')) + + +class PercentageStrategy(BaseSplitStrategy): + """比例分账策略 + + 配置示例: + { role: 'worker', type: 'percentage', value: 0.80 } + { role: 'worker', type: 'percentage', rate_table_key: '1' } + """ + + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + rate_key = self._rule.get('RateTableKey', '') + if rate_key: + rate = self._get_rate_from_table(rate_key) + else: + rate = Decimal(str(self._rule.get('Value', 0))) + amount = total_amount * rate + return self._apply_bounds(amount, total_amount) + + def _apply_bounds(self, amount, total_amount): + minimum = self._rule.get('MinAmount') + maximum = self._rule.get('MaxAmount') + if minimum is not None: + amount = max(amount, Decimal(str(minimum))) + if maximum is not None: + amount = min(amount, Decimal(str(maximum))) + return amount + + +class FixedStrategy(BaseSplitStrategy): + """固定金额策略 + + 配置示例: + { role: 'agent', type: 'fixed', value: 10 } + { role: 'leader', type: 'fixed', rate_table_key: 'guanshi_fc' } + """ + + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + rate_key = self._rule.get('RateTableKey', '') + if rate_key: + amount = self._get_rate_from_table(rate_key) + else: + amount = Decimal(str(self._rule.get('Value', 0))) + return amount + + +class TieredStrategy(BaseSplitStrategy): + """阶梯分账策略——按次数/档位查表 + + 配置示例: + { role: 'agent', type: 'tiered', + tiers: { 1: 5, 2: 8, 3: 10 } } + 或从费率表中按 key_N 形式查找。 + """ + + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + tiers = self._rule.get('Tiers', {}) + if tiers: + tier_key = str(tier_index) + raw_value = tiers.get(tier_key, tiers.get('default', 0)) + if isinstance(raw_value, str) and raw_value.endswith('%'): + return total_amount * Decimal(raw_value.rstrip('%')) / Decimal('100') + return Decimal(str(raw_value)) + + tier_key = self._rule.get('TierKeyPrefix', '') + if tier_key and self._rate_table: + rate = self._rate_table.GetRate(f'{tier_key}_{tier_index}') + if rate is not None: + return rate + + return Decimal('0') + + +class RemainderStrategy(BaseSplitStrategy): + """剩余归平台策略——扣除所有其他参与者后的余额 + + 配置示例: + { role: 'platform', type: 'remainder' } + """ + + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + allocated = context.get('AllocatedAmount', Decimal('0')) + remainder = total_amount - allocated + return remainder if remainder > 0 else Decimal('0') + + +class WithdrawalFeeStrategy(BaseSplitStrategy): + """提现手续费策略 + + 配置示例: + { role: 'platform', type: 'withdrawal_fee', value: 0.01 } + """ + + def Calculate(self, total_amount: Decimal, tier_index: int = 0, **context) -> Decimal: + rate_key = self._rule.get('RateTableKey', '') + if rate_key: + rate = self._get_rate_from_table(rate_key) + else: + rate = Decimal(str(self._rule.get('Value', 0))) + return total_amount * rate + + +STRATEGY_MAP: Dict[str, type] = { + 'percentage': PercentageStrategy, + 'fixed': FixedStrategy, + 'tiered': TieredStrategy, + 'remainder': RemainderStrategy, + 'withdrawal_fee': WithdrawalFeeStrategy, +} + + +def CreateStrategy(participant_rule: dict, rate_table: Optional[RateTable] = None) -> BaseSplitStrategy: + split_type = participant_rule.get('Type', 'percentage') + strategy_class = STRATEGY_MAP.get(split_type, PercentageStrategy) + return strategy_class(participant_rule, rate_table) diff --git a/gvsdsdk/db_router.py b/gvsdsdk/db_router.py new file mode 100644 index 0000000..1825720 --- /dev/null +++ b/gvsdsdk/db_router.py @@ -0,0 +1,93 @@ +"""gvsdsdk.db_router — 开箱即用的 SAAS 数据库路由器 + +子服务器只需在 settings.py 中配置: + DATABASE_ROUTERS = ['gvsdsdk.db_router.SAASDatabaseRouter'] + +即可自动将 gvsdsdk 的所有模型路由到 SAAS 主库。 + +配置项(settings.py): + GVSDSDK_SAAS_DB_ALIAS = 'saas' # SAAS 数据库别名,默认 'saas' + GVSDSDK_SAAS_READ_ONLY = True # 子服务器是否只读,默认 True +""" + + +class SAASDatabaseRouter: + """SAAS 模型数据库路由器——将 gvsdsdk 模型路由到 SAAS 主库 + + 用法: + # settings.py + DATABASES = { + 'default': { ... }, # 本地数据库 + 'saas': { # SAAS 主库 + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'Users', + ... + }, + } + DATABASE_ROUTERS = ['gvsdsdk.db_router.SAASDatabaseRouter'] + + # 可选配置 + GVSDSDK_SAAS_DB_ALIAS = 'saas' # 默认 'saas' + GVSDSDK_SAAS_READ_ONLY = True # 默认 True(子服务器只读) + """ + + # gvsdsdk 内所有模块前缀 + _GVSDSDK_MODULES = frozenset({ + 'gvsdsdk.models', + 'gvsdsdk.commission.models', + 'gvsdsdk.payment.models', + }) + + # gvsdsdk 内所有 app_label + _GVSDSDK_APP_LABELS = frozenset({ + 'gvsdsdk', + }) + + def _get_saas_db(self): + from django.conf import settings + return getattr(settings, 'GVSDSDK_SAAS_DB_ALIAS', 'saas') + + def _is_read_only(self): + from django.conf import settings + return getattr(settings, 'GVSDSDK_SAAS_READ_ONLY', True) + + def _is_gvsdsdk_model(self, model): + """判断模型是否属于 gvsdsdk""" + # 按模块路径判断 + module = getattr(model, '__module__', '') + if module in self._GVSDSDK_MODULES or module.startswith('gvsdsdk.'): + return True + # 按 app_label 判断 + app_label = getattr(model._meta, 'app_label', '') + if app_label in self._GVSDSDK_APP_LABELS: + return True + return False + + def db_for_read(self, model, **hints): + if self._is_gvsdsdk_model(model): + return self._get_saas_db() + return None + + def db_for_write(self, model, **hints): + if self._is_gvsdsdk_model(model): + if self._is_read_only(): + raise RuntimeError( + "gvsdsdk SAAS database is read-only for sub-servers. " + "Set GVSDSDK_SAAS_READ_ONLY=False to allow writes." + ) + return self._get_saas_db() + return None + + def allow_relation(self, obj1, obj2, **hints): + # 允许同一数据库内的关联 + db1 = self.db_for_read(obj1.__class__) + db2 = self.db_for_read(obj2.__class__) + if db1 and db2 and db1 == db2: + return True + return None + + def allow_migrate(self, db, app_label, model_name=None, **hints): + # gvsdsdk 模型不迁移(managed=False 或由主服务器管理) + if app_label in self._GVSDSDK_APP_LABELS: + return db != self._get_saas_db() + return None diff --git a/gvsdsdk/fluent.py b/gvsdsdk/fluent.py new file mode 100644 index 0000000..a8f9e96 --- /dev/null +++ b/gvsdsdk/fluent.py @@ -0,0 +1,651 @@ +import threading +import functools +from django.db import connection as default_db_connection, models, transaction +from django.db.models import Q, F, Count, Sum, Avg, Min, Max, Subquery, OuterRef, Case, When, Value +from gvsdsdk.model_base import FieldExpression + + +_local = threading.local() + + +class _AggregateExpr: + def __init__(self, model, aggregate): + self.model = model + self.aggregate = aggregate + + +class _ExtractExpr: + def __init__(self, field_name, lookup_type): + self.field_name = field_name + self.lookup_type = lookup_type + + def __eq__(self, other): + return Q(**{f'{self.field_name}__{self.lookup_type}': other}) + + def __and__(self, other): + if isinstance(other, Q): + return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other + return self.__eq__(other) + + +class _CompatFunc: + def sum(self, field_expr): + if hasattr(field_expr, '_django_name'): + agg = Sum(field_expr._django_name) + elif hasattr(field_expr, 'field_name'): + agg = Sum(field_expr.field_name) + else: + agg = Sum(str(field_expr)) + model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + return _AggregateExpr(model, agg) if model else agg + + def count(self, field_expr=None): + if field_expr is None: + return Count('*') + if hasattr(field_expr, '_django_name'): + agg = Count(field_expr._django_name) + else: + agg = Count(str(field_expr)) + model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + return _AggregateExpr(model, agg) if model else agg + + def max(self, field_expr): + if hasattr(field_expr, '_django_name'): + agg = Max(field_expr._django_name) + else: + agg = Max(str(field_expr)) + model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + return _AggregateExpr(model, agg) if model else agg + + def min(self, field_expr): + if hasattr(field_expr, '_django_name'): + agg = Min(field_expr._django_name) + else: + agg = Min(str(field_expr)) + model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + return _AggregateExpr(model, agg) if model else agg + + def avg(self, field_expr): + if hasattr(field_expr, '_django_name'): + agg = Avg(field_expr._django_name) + else: + agg = Avg(str(field_expr)) + model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + return _AggregateExpr(model, agg) if model else agg + + def year(self, field_expr): + if hasattr(field_expr, '_django_name'): + return _ExtractExpr(field_expr._django_name, 'year') + return _ExtractExpr(str(field_expr), 'year') + + def month(self, field_expr): + if hasattr(field_expr, '_django_name'): + return _ExtractExpr(field_expr._django_name, 'month') + return _ExtractExpr(str(field_expr), 'month') + + def day(self, field_expr): + if hasattr(field_expr, '_django_name'): + return _ExtractExpr(field_expr._django_name, 'day') + return _ExtractExpr(str(field_expr), 'day') + + +func = _CompatFunc() + + +class _DeleteStmt: + def __init__(self, model_class, conditions=None): + self.model_class = model_class + self.conditions = conditions or Q() + + def where(self, *args, **kwargs): + new_conditions = Q() + for a in args: + if isinstance(a, Q): + new_conditions &= a + self.conditions = new_conditions + return self + + def execute(self): + if self.model_class and self.conditions: + self.model_class.objects.filter(self.conditions).delete() + elif self.model_class: + self.model_class.objects.all().delete() + + +def _get_session(): + if not hasattr(_local, 'db_session'): + _local.db_session = Session() + return _local.db_session + + +def _get_cache(): + if not hasattr(_local, 'query_cache'): + _local.query_cache = {} + return _local.query_cache + + +def _invalidate_cache(): + if hasattr(_local, 'query_cache'): + _local.query_cache.clear() + + +class RelationAccessor: + def __init__(self, model_class, prefix=''): + self._model = model_class + self._prefix = prefix + + def __getattr__(self, name): + if name.startswith('_'): + return object.__getattribute__(self, name) + try: + self._model._meta.get_field(name) + full_name = f'{self._prefix}__{name}' if self._prefix else name + return FieldExpression(self._model, full_name) + except Exception: + for rel in self._model._meta.get_fields(include_hidden=True): + if hasattr(rel, 'related_model') and rel.related_model: + rel_name = '' + if hasattr(rel, 'get_accessor_name'): + rel_name = rel.get_accessor_name() + elif hasattr(rel, 'name'): + rel_name = rel.name + if rel_name and rel_name == name: + full_prefix = f'{self._prefix}__{rel_name}' if self._prefix else rel_name + return RelationAccessor(rel.related_model, full_prefix) + return FieldExpression(self._model, name) + + +class _ValuesResult: + """ + 包装 Django ValuesQuerySet,支持链式调用。 + 底层使用 Django .values() 返回字典,保持与 Django 行为一致。 + 额外提供 .all() 返回字典列表,.first() 返回字典或 None, + .scalar() 返回单字段场景的标量值。 + """ + def __init__(self, qs, fields, original_fields): + self._qs = qs # Django ValuesQuerySet(返回字典) + self._fields = fields # 转换后的字段名列表(传给 Django .values() 的) + self._original_fields = original_fields # 原始传入的字段(用于判断单字段场景) + + def filter(self, *args, **kwargs): + self._qs = self._qs.filter(*args, **kwargs) + return self + + def exclude(self, *args, **kwargs): + self._qs = self._qs.exclude(*args, **kwargs) + return self + + def order_by(self, *fields): + self._qs = self._qs.order_by(*fields) + return self + + def distinct(self): + self._qs = self._qs.distinct() + return self + + def annotate(self, **kwargs): + self._qs = self._qs.annotate(**kwargs) + return self + + def first(self): + return self._qs.first() + + def all(self): + return list(self._qs) + + def scalar(self): + row = self._qs.first() + if row is None: + return None + if self._fields: + return row.get(self._fields[0]) + return row + + def count(self): + return self._qs.count() + + def exists(self): + return self._qs.exists() + + def __iter__(self): + return iter(self._qs) + + def __len__(self): + return self.count() + + def __bool__(self): + return self._qs.exists() + + +class FluentQuery: + def __init__(self, model_class): + self._model = model_class + self._qs = model_class.objects.all() + self._joined_models = [] + self._cache_enabled = True + + def _clone(self): + q = FluentQuery(self._model) + q._qs = self._qs + q._joined_models = list(self._joined_models) + q._cache_enabled = self._cache_enabled + return q + + def _cache_key(self, method): + try: + query_str = str(self._qs.query) + except Exception: + query_str = id(self._qs) + return (self._model, method, query_str) + + def _remap_q(self, q): + new_children = [] + for child in q.children: + if isinstance(child, Q): + new_children.append(self._remap_q(child)) + elif isinstance(child, tuple): + key, val = child + new_key = self._remap_key(key) + new_children.append((new_key, val)) + else: + new_children.append(child) + if new_children: + new_q = Q(*new_children) + new_q.connector = q.connector + new_q.negated = q.negated + return new_q + return q + + def _remap_key(self, key): + parts = key.split('__') + field_name = parts[0] + try: + self._model._meta.get_field(field_name) + return key + except Exception: + pass + for joined in self._joined_models: + try: + joined._meta.get_field(field_name) + for rel in self._model._meta.get_fields(include_hidden=True): + if hasattr(rel, 'related_model') and rel.related_model == joined: + acc = '' + if hasattr(rel, 'get_accessor_name'): + acc = rel.get_accessor_name() + elif hasattr(rel, 'name'): + acc = rel.name + if acc and acc != '+': + return acc + '__' + key + return joined._meta.model_name + '__' + key + except Exception: + pass + return key + + def filter(self, *args, **kwargs): + converted_args = [] + for arg in args: + if isinstance(arg, Q) and self._joined_models: + converted_args.append(self._remap_q(arg)) + else: + converted_args.append(arg) + self._qs = self._qs.filter(*converted_args, **kwargs) + return self + + def exclude(self, *args, **kwargs): + converted_args = [] + for arg in args: + if isinstance(arg, Q) and self._joined_models: + converted_args.append(self._remap_q(arg)) + else: + converted_args.append(arg) + self._qs = self._qs.exclude(*converted_args, **kwargs) + return self + + def order_by(self, *args): + converted = [] + for a in args: + if isinstance(a, str): + converted.append(a) + elif isinstance(a, FieldExpression): + converted.append(str(a)) + else: + converted.append(str(a)) + self._qs = self._qs.order_by(*converted) + return self + + def first(self): + if self._cache_enabled: + cache = _get_cache() + key = self._cache_key('first') + if key in cache: + return cache[key] + obj = self._qs.first() + if obj is not None: + _get_session().track(obj) + if self._cache_enabled: + _get_cache()[self._cache_key('first')] = obj + return obj + + def all(self): + """返回 self 以支持链式调用(与 Django QuerySet.all() 一致)。 + 如需获取列表,使用 .to_list() 或 list()。""" + self._qs = self._qs.all() + return self + + def to_list(self): + """执行查询并返回模型实例列表。""" + if self._cache_enabled: + cache = _get_cache() + key = self._cache_key('to_list') + if key in cache: + return cache[key] + results = list(self._qs) + session = _get_session() + for obj in results: + session.track(obj) + if self._cache_enabled: + _get_cache()[self._cache_key('to_list')] = results + return results + + def count(self): + if self._cache_enabled: + cache = _get_cache() + key = self._cache_key('count') + if key in cache: + return cache[key] + result = self._qs.count() + if self._cache_enabled: + _get_cache()[self._cache_key('count')] = result + return result + + def get(self, *args, **kwargs): + if args: + obj = self._qs.filter(*args).get(**kwargs) + else: + obj = self._qs.get(**kwargs) + _get_session().track(obj) + return obj + + def create(self, **kwargs): + obj = self._model.objects.create(**kwargs) + _invalidate_cache() + return obj + + def get_or_create(self, defaults=None, **kwargs): + obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs) + if created: + _invalidate_cache() + _get_session().track(obj) + return obj, created + + def update_or_create(self, defaults=None, **kwargs): + obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs) + _invalidate_cache() + _get_session().track(obj) + return obj, created + + def none(self): + self._qs = self._qs.none() + return self + + def last(self): + return self._qs.last() + + def limit(self, n): + self._qs = self._qs[:n] + return self + + def offset(self, n): + self._qs = self._qs[n:] + return self + + def join(self, *args): + for arg in args: + if isinstance(arg, type) and issubclass(arg, models.Model): + if arg not in self._joined_models: + self._joined_models.append(arg) + return self + + def select_related(self, *fields): + self._qs = self._qs.select_related(*fields) + return self + + def prefetch_related(self, *fields): + self._qs = self._qs.prefetch_related(*fields) + return self + + def only(self, *fields): + self._qs = self._qs.only(*fields) + return self + + def defer(self, *fields): + self._qs = self._qs.defer(*fields) + return self + + def select_for_update(self, nowait=False, skip_locked=False, of=(), no_key=False): + self._qs = self._qs.select_for_update( + nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key + ) + return self + + def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): + result = self._model.objects.bulk_create( + objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts + ) + _invalidate_cache() + return result + + def annotate(self, **kwargs): + self._qs = self._qs.annotate(**kwargs) + return self + + def aggregate(self, **kwargs): + return self._qs.aggregate(**kwargs) + + def values(self, *fields): + if not fields: + # 无参数:返回 Django .values()(所有字段字典) + return _ValuesResult(self._qs.values(), [], []) + + annotations = {} + converted = [] + for i, f in enumerate(fields): + if isinstance(f, _AggregateExpr): + annotations[f'val{i}'] = f.aggregate + converted.append(f'val{i}') + elif isinstance(f, (models.Aggregate, models.Expression)): + annotations[f'val{i}'] = f + converted.append(f'val{i}') + elif isinstance(f, str): + converted.append(f) + else: + converted.append(str(f)) + + qs = self._qs + if annotations: + qs = qs.annotate(**annotations) + # 使用 Django 原生 .values() 返回字典 + qs = qs.values(*converted) + return _ValuesResult(qs, converted, fields) + + def with_entities(self, *fields): + return self.values(*fields) + + def params(self, **kwargs): + return self + + def values_list(self, *fields, flat=False): + return self._qs.values_list(*fields, flat=flat) + + def update(self, **kwargs): + result = self._qs.update(**kwargs) + _invalidate_cache() + return result + + def delete(self): + result = self._qs.delete() + _invalidate_cache() + return result + + def exists(self): + return self._qs.exists() + + def distinct(self): + self._qs = self._qs.distinct() + return self + + def paginate(self, page=1, per_page=20, error_out=False): + from django.core.paginator import Paginator + if not self._qs.ordered: + meta = getattr(self._model, '_meta', None) + if meta and meta.pk: + self._qs = self._qs.order_by(meta.pk.name) + paginator = Paginator(self._qs, per_page) + try: + page_obj = paginator.page(page) + except Exception: + page_obj = paginator.page(1) + page_obj.total = paginator.count + page_obj.items = page_obj.object_list + page_obj.pages = paginator.num_pages + page_obj.page = page + page_obj.per_page = per_page + page_obj.has_next = page_obj.has_next() + page_obj.has_prev = page_obj.has_previous() + page_obj.next_num = page_obj.next_page_number() if page_obj.has_next else None + page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None + return page_obj + + def no_cache(self): + self._cache_enabled = False + return self + + def using(self, alias): + self._qs = self._qs.using(alias) + return self + + def __getitem__(self, key): + """支持切片 [start:end] 和索引 [n]""" + if isinstance(key, slice): + self._qs = self._qs[key] + return self + else: + # 单个索引,返回模型实例 + return self._qs[key] + + def __iter__(self): + return iter(self._qs) + + def __len__(self): + return self.count() + + def __bool__(self): + return self.exists() + + def __repr__(self): + return f'FluentQuery({self._model.__name__})' + + +class Session: + def __init__(self): + self._pending_adds = [] + self._pending_deletes = [] + self._tracked = set() + + def add(self, obj): + self._pending_adds.append(obj) + + def delete(self, obj): + self._pending_deletes.append(obj) + self._tracked.discard(obj) + + def track(self, obj): + if isinstance(obj, models.Model): + self._tracked.add(obj) + + def commit(self): + delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None} + with transaction.atomic(): + for obj in self._pending_deletes: + obj.delete() + for obj in self._pending_adds: + obj.save() + for obj in self._tracked: + if obj.pk is not None and (obj.__class__, obj.pk) in delete_pks: + continue + try: + obj.save() + except Exception: + pass + self._pending_adds.clear() + self._pending_deletes.clear() + self._tracked.clear() + _invalidate_cache() + + def rollback(self): + self._pending_adds.clear() + self._pending_deletes.clear() + self._tracked.clear() + _invalidate_cache() + + def query(self, *args): + model = None + aggregates = [] + for a in args: + if isinstance(a, type) and issubclass(a, models.Model): + model = a + elif isinstance(a, _AggregateExpr): + if a.model: + model = a.model + aggregates.append(a.aggregate) + elif isinstance(a, (models.Aggregate, models.Expression)): + aggregates.append(a) + if not aggregates and model: + return FluentQuery(model) + if aggregates and model: + fq = FluentQuery(model) + return fq.values(*args) + return None + + def flush(self): + self.commit() + + def execute(self, stmt): + if isinstance(stmt, _DeleteStmt): + stmt.execute() + elif isinstance(stmt, str): + connection = default_db_connection + with connection.cursor() as cursor: + cursor.execute(stmt) + + +class DB: + @property + def session(self): + return _get_session() + + @staticmethod + def or_(*args): + result = None + for q in args: + if isinstance(q, Q): + if result is None: + result = q + else: + result |= q + return result if result is not None else Q() + + @staticmethod + def and_(*args): + result = Q() + for q in args: + if isinstance(q, Q): + result &= q + return result + + +db = DB() + + +def FQ(model_class): + return FluentQuery(model_class) diff --git a/gvsdsdk/form_service.py b/gvsdsdk/form_service.py new file mode 100644 index 0000000..cac636b --- /dev/null +++ b/gvsdsdk/form_service.py @@ -0,0 +1,417 @@ +"""模块:通用 FormService 引擎 + +提供子服务器开箱即用的 ORM 表管理 API。 +子服务器只需继承 FormServiceBase 并注册 TABLE_MAP 即可。 + +使用方式: + # 子服务器 form_service.py + from gvsdsdk.form_service import FormServiceBase + from .models import MyModel1, MyModel2 + + class FormService(FormServiceBase): + TABLE_MAP = { + 'MyModel1': MyModel1, + 'MyModel2': MyModel2, + } + + # 子服务器 views.py + from gvsdsdk.form_service import FormServiceAPIView + from .form_service import FormService +""" + +import uuid +import datetime +import logging +from django.db.models import Q + +logger = logging.getLogger(__name__) + + +def _pascalize_record(record, model=None): + """将数据库记录的键名转为 PascalCase(使用 db_column)""" + result = {} + column_map = {} + if model is not None: + for f in model._meta.get_fields(): + if hasattr(f, 'db_column') and f.db_column: + column_map[f.name] = f.db_column + if hasattr(f, 'attname') and f.attname != f.name: + column_map[f.attname] = f.db_column + for key, val in record.items(): + out_key = column_map.get(key, key) + if isinstance(val, bytes) and len(val) == 16: + try: + val = str(uuid.UUID(bytes=val)) + except ValueError: + pass + elif isinstance(val, datetime.datetime): + val = val.strftime('%Y-%m-%d %H:%M:%S') + elif isinstance(val, datetime.date): + val = val.isoformat() + result[out_key] = val + return result + + +class FormServiceBase: + """通用 FormService 基类 + + 子类需要定义: + TABLE_MAP: dict — 表名到 Django Model 的映射 + ENTITY_PICKER_MAP: dict — ForeignKey 到实体选择器的映射(可选) + READONLY_TABLES_FOR_NORMAL: set — 普通用户只读的表(可选) + ADMIN_ONLY_TABLES: set — 仅管理员可访问的表(可选) + """ + + TABLE_MAP = {} + ENTITY_PICKER_MAP = {} + READONLY_TABLES_FOR_NORMAL = set() + ADMIN_ONLY_TABLES = set() + + def __init__(self, user=None): + self.user = user + + def _build_field_map(self, model): + pascal_map = {} + for f in model._meta.get_fields(): + if hasattr(f, 'name'): + pascal_map[f.name] = f + return pascal_map + + def _resolve_field_name(self, key, pascal_map): + if key in pascal_map: + return pascal_map[key].name + return None + + def _convert_value_for_field(self, field, val): + if val is None or val == '': + if getattr(field, 'null', False): + return None + return val + field_type = field.__class__.__name__ + if field_type == 'UUIDField' and isinstance(val, str): + try: + return uuid.UUID(val).bytes + except ValueError: + return val + if field_type == 'BinaryField' and isinstance(val, str): + try: + return uuid.UUID(val).bytes + except ValueError: + return val + if field_type == 'ForeignKey' and isinstance(val, str): + try: + return uuid.UUID(val).bytes + except ValueError: + return val + if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField', + 'PositiveIntegerField', 'PositiveSmallIntegerField', + 'FloatField', 'DecimalField') and isinstance(val, str): + try: + if field_type == 'FloatField': + return float(val) + return int(val) + except (ValueError, TypeError): + return val + if field_type == 'DateTimeField' and isinstance(val, str): + try: + return datetime.datetime.fromisoformat(val) + except (ValueError, TypeError): + return val + if hasattr(field, 'max_length') and isinstance(val, str) and len(val) > field.max_length: + val = val[:field.max_length] + return val + + @staticmethod + def _map_field_type(field): + t = field.__class__.__name__ + mapping = { + 'UUIDField': 'uuid', + 'BinaryField': 'uuid', + 'FixedBinaryField': 'uuid', + 'CharField': 'text', + 'TextField': 'textarea', + 'JSONField': 'textarea', + 'SmallIntegerField': 'number', + 'IntegerField': 'number', + 'BigIntegerField': 'number', + 'PositiveIntegerField': 'number', + 'PositiveSmallIntegerField': 'number', + 'FloatField': 'number', + 'DecimalField': 'number', + 'BooleanField': 'boolean', + 'NullBooleanField': 'boolean', + 'DateTimeField': 'datetime', + 'DateField': 'date', + 'TimeField': 'time', + 'ForeignKey': 'uuid', + 'OneToOneField': 'uuid', + 'ManyToManyField': 'text', + 'ImageField': 'text', + 'FileField': 'text', + 'SlugField': 'text', + 'EmailField': 'text', + 'URLField': 'text', + 'AutoField': 'number', + 'BigAutoField': 'number', + } + if t == 'CharField' and hasattr(field, 'choices') and field.choices: + return 'select' + return mapping.get(t, 'text') + + def get_tables(self): + tables = [] + for name, model in self.TABLE_MAP.items(): + try: + count = model.objects.count() + except Exception: + count = -1 + tables.append({ + 'name': name, + 'title': name, + 'count': count, + 'type': 'dynamic', + 'source': 'model', + }) + return tables + + def get_columns(self, table_name): + model = self.TABLE_MAP.get(table_name) + if not model: + return [] + try: + columns = [] + for f in model._meta.get_fields(): + if not hasattr(f, 'name') or f.name in ('id',): + continue + if f.auto_created and not f.concrete: + continue + if f.is_relation and not f.concrete: + continue + is_pk = (f.name == model._meta.pk.name) if model._meta.pk else False + pascal_name = f.name + field_type = self._map_field_type(f) + col = { + 'field': pascal_name, + 'title': pascal_name, + 'type': field_type, + 'editable': getattr(f, 'editable', True) and not is_pk, + 'sortable': True, + 'filterable': True, + 'primaryKey': is_pk, + 'hideInForm': is_pk, + } + if field_type == 'select' and hasattr(f, 'choices') and f.choices: + col['options'] = [{'value': k, 'label': str(v)} for k, v in f.choices] + if field_type == 'number': + col['min'] = None + col['max'] = None + for v in getattr(f, 'validators', []): + vname = v.__class__.__name__ + if vname == 'MinValueValidator': + col['min'] = v.limit_value + elif vname == 'MaxValueValidator': + col['max'] = v.limit_value + if col['min'] is None and col['max'] is None: + col.pop('min', None) + col.pop('max', None) + if field_type == 'text' and hasattr(f, 'max_length') and f.max_length: + col['maxLength'] = f.max_length + if getattr(f, 'null', False): + col['required'] = False + # ForeignKey 字段:检测关联模型,自动使用实体选择器 + if f.__class__.__name__ == 'ForeignKey': + related_model = getattr(f, 'related_model', None) + if related_model: + related_name = related_model.__name__ + ep_config = self.ENTITY_PICKER_MAP.get(related_name) + if ep_config: + col['type'] = 'entityPicker' + col['entityPicker'] = ep_config + columns.append(col) + return columns + except Exception: + return [] + + def get_table(self, table_name, params): + model = self.TABLE_MAP.get(table_name) + if not model: + return {'records': [], 'total': 0} + qs = model.objects.all() + keyword = params.get('keyword', '') + if keyword: + or_conditions = {} + for f in model._meta.get_fields(): + if hasattr(f, 'name') and f.__class__.__name__ in ('CharField', 'TextField'): + or_conditions[f'{f.name}__icontains'] = keyword + if or_conditions: + q = Q() + for k, v in or_conditions.items(): + q |= Q(**{k: v}) + qs = qs.filter(q) + try: + page = int(params.get('PageIndex', 1)) + except (ValueError, TypeError): + page = 1 + try: + page_size = int(params.get('PageSize', 20)) + except (ValueError, TypeError): + page_size = 20 + total = qs.count() + start = (page - 1) * page_size + records = [_pascalize_record(r, model) for r in qs[start:start + page_size].values()] + return {'records': records, 'total': total, 'page': page, 'PageSize': page_size} + + def add_record(self, data): + table_name = data.get('Table', '') + model = self.TABLE_MAP.get(table_name) + if not model: + return None + pascal_map = self._build_field_map(model) + create_data = {} + for key, val in data.items(): + if key in ('table', 'action', 'Table', 'Action', 'pk', 'PK', 'uuid', 'UUID'): + continue + resolved = self._resolve_field_name(key, pascal_map) + if resolved is None: + continue + field = pascal_map.get(resolved) or pascal_map.get(key) + if field is None: + continue + if isinstance(val, list) and len(val) == 1: + val = val[0] + val = self._convert_value_for_field(field, val) + is_fk = field.__class__.__name__ == 'ForeignKey' + attr_name = field.attname if is_fk else resolved + create_data[attr_name] = val + pk_field_name = model._meta.pk.name if model._meta.pk else None + if pk_field_name and pk_field_name not in create_data: + pk_field = model._meta.pk + if pk_field.__class__.__name__ in ('UUIDField', 'BinaryField', 'FixedBinaryField'): + create_data[pk_field_name] = uuid.uuid4().bytes + obj = model.objects.create(**create_data) + return {'PK': str(obj.pk) if obj.pk else None} + + def modify_record(self, data): + table_name = data.get('Table', '') + model = self.TABLE_MAP.get(table_name) + if not model: + return None + pk = data.get('PK') + if not pk: + return None + pk_field_name = model._meta.pk.name + pk_field = model._meta.pk + pk_value = self._convert_value_for_field(pk_field, pk) + try: + obj = model.objects.get(**{pk_field_name: pk_value}) + except model.DoesNotExist: + return None + pascal_map = self._build_field_map(model) + update_fields = [] + for key, val in data.items(): + if key in ('table', 'action', 'pk', 'uuid', 'Table', 'Action', 'PK', 'UUID'): + continue + resolved = self._resolve_field_name(key, pascal_map) + if resolved is None: + continue + if resolved == model._meta.pk.name: + continue + field = pascal_map.get(resolved) or pascal_map.get(key) + if field is None: + continue + if isinstance(val, list) and len(val) == 1: + val = val[0] + val = self._convert_value_for_field(field, val) + is_fk = field.__class__.__name__ == 'ForeignKey' + attr_name = field.attname if is_fk else resolved + setattr(obj, attr_name, val) + update_fields.append(attr_name) + if update_fields: + obj.save(update_fields=update_fields) + return {'PK': str(obj.pk) if obj.pk else None} + + def modify_item(self, data): + return self.modify_record(data) + + def delete_record(self, data): + table_name = data.get('Table', '') + model = self.TABLE_MAP.get(table_name) + if not model: + return False + pk = data.get('PK') + if not pk: + return False + pk_field_name = model._meta.pk.name + pk_field = model._meta.pk + pk_value = self._convert_value_for_field(pk_field, pk) + try: + model.objects.get(**{pk_field_name: pk_value}).delete() + return True + except model.DoesNotExist: + return False + + def code_display(self, data): + table_name = data.get('Table', '') + code_field = data.get('CodeField', 'PermCode') + name_field = data.get('NameField', 'PermName') + model = self.TABLE_MAP.get(table_name) + if not model: + return [] + fields = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')} + if code_field not in fields or name_field not in fields: + return [] + qs = model.objects.all() + keyword = data.get('Keyword', '') + if keyword: + filter_kw = {f'{code_field}__icontains': keyword} + qs = qs.filter(**filter_kw) + results = [] + for obj in qs[:50]: + results.append({ + 'code': getattr(obj, code_field, ''), + 'name': getattr(obj, name_field, ''), + }) + return results + + def handle_request(self, action, data): + """统一请求入口 + + Args: + action: 操作类型 (tables/columns/list/add/modify/delete/code_display) + data: 请求数据 + + Returns: + dict 响应数据 + """ + if action == 'tables': + return {'code': 0, 'msg': 'success', 'data': self.get_tables()} + elif action == 'columns': + table_name = data.get('Table', '') + if table_name not in self.TABLE_MAP: + return {'code': 1, 'msg': f'表 {table_name} 不存在'} + return {'code': 0, 'msg': 'success', 'data': self.get_columns(table_name)} + elif action == 'list': + table_name = data.get('Table', '') + if table_name not in self.TABLE_MAP: + return {'code': 1, 'msg': f'表 {table_name} 不存在'} + return {'code': 0, 'msg': 'success', 'data': self.get_table(table_name, data)} + elif action == 'add': + result = self.add_record(data) + if result is None: + return {'code': 1, 'msg': '添加失败'} + return {'code': 0, 'msg': 'success', 'data': result} + elif action == 'modify': + result = self.modify_record(data) + if result is None: + return {'code': 1, 'msg': '修改失败'} + return {'code': 0, 'msg': 'success', 'data': result} + elif action == 'delete': + result = self.delete_record(data) + if not result: + return {'code': 1, 'msg': '删除失败'} + return {'code': 0, 'msg': 'success'} + elif action == 'code_display': + result = self.code_display(data) + return {'code': 0, 'msg': 'success', 'data': result} + else: + return {'code': 1, 'msg': f'未知操作: {action}'} diff --git a/gvsdsdk/management/__init__.py b/gvsdsdk/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gvsdsdk/management/commands/__init__.py b/gvsdsdk/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gvsdsdk/management/commands/gvsdsdk_init.py b/gvsdsdk/management/commands/gvsdsdk_init.py new file mode 100644 index 0000000..82c06be --- /dev/null +++ b/gvsdsdk/management/commands/gvsdsdk_init.py @@ -0,0 +1,135 @@ +"""gvsdsdk init — 子服务器快速初始化 SAAS 表 + +用法: + python manage.py gvsdsdk_init --db saas + python manage.py gvsdsdk_init --db saas --dry-run # 仅打印 SQL + python manage.py gvsdsdk_init --db default --modules auth,commission,payment + +此命令会根据 gvsdsdk 中的模型定义,在指定数据库中创建对应的表。 +适用于子服务器独立部署时快速建表。 +""" + +from django.core.management.base import BaseCommand, CommandError +from django.db import connections +from django.apps import apps + + +class Command(BaseCommand): + help = '初始化 gvsdsdk SAAS 表结构(子服务器建表工具)' + + def add_arguments(self, parser): + parser.add_argument( + '--db', default='saas', + help='目标数据库别名(默认 saas)', + ) + parser.add_argument( + '--dry-run', action='store_true', + help='仅打印建表 SQL,不执行', + ) + parser.add_argument( + '--modules', default='all', + help='要初始化的模块,逗号分隔(all, auth, commission, payment)。默认 all', + ) + + def handle(self, *args, **options): + db_alias = options['db'] + dry_run = options['dry_run'] + modules_str = options['modules'] + + # 验证数据库连接 + try: + connection = connections[db_alias] + except KeyError: + raise CommandError(f'数据库别名 "{db_alias}" 未配置,请在 DATABASES 中添加') + + # 收集 gvsdsdk 模型 + target_modules = self._parse_modules(modules_str) + models_to_create = self._collect_models(target_modules) + + if not models_to_create: + self.stdout.write(self.style.WARNING('没有找到需要创建的模型')) + return + + self.stdout.write(f'找到 {len(models_to_create)} 个模型:') + for model in models_to_create: + self.stdout.write(f' - {model.__name__} → {model._meta.db_table}') + + # 生成建表 SQL + schema_editor = connection.schema_editor(collect_sql=True) + for model in models_to_create: + try: + schema_editor.create_model(model) + except Exception as e: + self.stdout.write(self.style.ERROR(f' 生成 {model.__name__} SQL 失败: {e}')) + + sql_statements = schema_editor.collected_sql + + if dry_run: + self.stdout.write(self.style.SUCCESS('\n=== DRY RUN: 建表 SQL ===')) + for sql in sql_statements: + self.stdout.write(sql) + self.stdout.write(self.style.SUCCESS(f'\n共 {len(sql_statements)} 条 SQL')) + return + + # 执行建表 + self.stdout.write(f'\n在数据库 "{db_alias}" 中创建表...') + try: + with connection.cursor() as cursor: + for sql in sql_statements: + cursor.execute(sql) + self.stdout.write(self.style.SUCCESS(f'成功创建 {len(sql_statements)} 个表')) + except Exception as e: + raise CommandError(f'建表失败: {e}') + + def _parse_modules(self, modules_str): + if modules_str == 'all': + return {'auth', 'commission', 'payment'} + return set(m.strip() for m in modules_str.split(',') if m.strip()) + + def _collect_models(self, target_modules): + """收集 gvsdsdk 中需要建表的模型""" + all_models = [] + + # 核心模型(auth 相关:User, Role, Permission, UserRole 等) + if 'auth' in target_modules: + try: + from gvsdsdk import models as core_models + for name in dir(core_models): + obj = getattr(core_models, name) + if self._is_qmodel(obj): + all_models.append(obj) + except Exception: + pass + + # 分佣模型 + if 'commission' in target_modules: + try: + from gvsdsdk.commission import models as comm_models + for name in dir(comm_models): + obj = getattr(comm_models, name) + if self._is_qmodel(obj): + all_models.append(obj) + except Exception: + pass + + # 支付模型 + if 'payment' in target_modules: + try: + from gvsdsdk.payment import models as pay_models + for name in dir(pay_models): + obj = getattr(pay_models, name) + if self._is_qmodel(obj): + all_models.append(obj) + except Exception: + pass + + return all_models + + def _is_qmodel(self, obj): + """判断是否为 QModel 子类(非抽象)""" + from gvsdsdk.model_base import QModel + return ( + isinstance(obj, type) + and issubclass(obj, QModel) + and not obj._meta.abstract + ) diff --git a/gvsdsdk/migrations/0001_initial.py b/gvsdsdk/migrations/0001_initial.py new file mode 100644 index 0000000..3a8544f --- /dev/null +++ b/gvsdsdk/migrations/0001_initial.py @@ -0,0 +1,231 @@ +# Generated by Django 6.0.5 on 2026-06-13 11:43 + +import datetime +import gvsdsdk.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AccountActivation', + fields=[ + ('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '账户激活', + 'verbose_name_plural': '账户激活', + 'db_table': 'AccountActivation', + 'managed': True, + }, + ), + migrations.CreateModel( + name='EphemeralToken', + fields=[ + ('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)), + ('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)), + ('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)), + ('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)), + ('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)), + ('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)), + ('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)), + ], + options={ + 'verbose_name': '临时令牌', + 'verbose_name_plural': '临时令牌', + 'db_table': 'EphemeralToken', + 'managed': True, + }, + ), + migrations.CreateModel( + name='Password', + fields=[ + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), + ('Password', models.CharField(db_column='Password', max_length=1024)), + ], + options={ + 'verbose_name': '密码', + 'verbose_name_plural': '密码', + 'db_table': 'Password', + 'managed': True, + }, + ), + migrations.CreateModel( + name='Permission', + fields=[ + ('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)), + ('PermName', models.CharField(db_column='PermName', max_length=128)), + ('PermCode', models.CharField(db_column='PermCode', max_length=128)), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), + ('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)), + ('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)), + ], + options={ + 'verbose_name': '权限', + 'verbose_name_plural': '权限', + 'db_table': 'Permission', + 'managed': True, + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)), + ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')), + ('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)), + ('RoleName', models.CharField(db_column='RoleName', max_length=128)), + ('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)), + ('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '角色', + 'verbose_name_plural': '角色', + 'db_table': 'Role', + 'managed': True, + }, + ), + migrations.CreateModel( + name='RoleDept', + fields=[ + ('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), + ('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')), + ('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)), + ], + options={ + 'verbose_name': '角色部门关联', + 'verbose_name_plural': '角色部门关联', + 'db_table': 'RoleDept', + 'managed': True, + }, + ), + migrations.CreateModel( + name='User', + fields=[ + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), + ('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)), + ('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)), + ('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)), + ('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)), + ('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)), + ('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)), + ('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)), + ('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)), + ('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)), + ('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)), + ('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)), + ('IsStaff', models.BooleanField(db_column='IsStaff', default=False)), + ('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)), + ], + options={ + 'verbose_name': '用户', + 'verbose_name_plural': '用户', + 'db_table': 'User', + 'managed': True, + }, + ), + migrations.CreateModel( + name='User2FA', + fields=[ + ('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)), + ('SecretKey', models.CharField(db_column='SecretKey', max_length=64)), + ('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)), + ('Verified', models.SmallIntegerField(db_column='Verified', default=0)), + ('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '双因素认证', + 'verbose_name_plural': '双因素认证', + 'db_table': 'User2FA', + 'managed': True, + }, + ), + migrations.CreateModel( + name='UserABAC', + fields=[ + ('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), + ('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)), + ], + options={ + 'verbose_name': '用户ABAC', + 'verbose_name_plural': '用户ABAC', + 'db_table': 'UserABAC', + 'managed': True, + }, + ), + migrations.CreateModel( + name='UserAvatar', + fields=[ + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), + ('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)), + ('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)), + ('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)), + ], + options={ + 'verbose_name': '用户头像', + 'verbose_name_plural': '用户头像', + 'db_table': 'UserAvatar', + 'managed': True, + }, + ), + migrations.CreateModel( + name='UserLoginLog', + fields=[ + ('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)), + ('LoginIP', models.CharField(db_column='LoginIP', max_length=39)), + ('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)), + ('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')), + ('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)), + ('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)), + ], + options={ + 'verbose_name': '登录日志', + 'verbose_name_plural': '登录日志', + 'db_table': 'UserLoginLog', + 'managed': True, + }, + ), + migrations.CreateModel( + name='UserPending2FA', + fields=[ + ('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)), + ('SecretKey', models.CharField(db_column='SecretKey', max_length=64)), + ('ExpireAt', models.DateTimeField(db_column='ExpireAt')), + ], + options={ + 'verbose_name': '2FA待确认', + 'verbose_name_plural': '2FA待确认', + 'db_table': 'UserPending2FA', + 'managed': True, + }, + ), + migrations.CreateModel( + name='UserRole', + fields=[ + ('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '用户角色关联', + 'verbose_name_plural': '用户角色关联', + 'db_table': 'UserRole', + 'managed': True, + }, + ), + ] diff --git a/gvsdsdk/migrations/0002_position_roleposition_roleshop_shop.py b/gvsdsdk/migrations/0002_position_roleposition_roleshop_shop.py new file mode 100644 index 0000000..8251940 --- /dev/null +++ b/gvsdsdk/migrations/0002_position_roleposition_roleshop_shop.py @@ -0,0 +1,90 @@ +# Generated by Django 6.0.5 on 2026-06-13 12:29 + +import datetime +import gvsdsdk.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gvsdsdk', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Position', + fields=[ + ('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False)), + ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')), + ('PositionName', models.CharField(db_column='PositionName', max_length=128)), + ('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64)), + ('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0)), + ('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1)), + ('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '职务', + 'verbose_name_plural': '职务', + 'db_table': 'Position', + 'managed': True, + }, + ), + migrations.CreateModel( + name='RolePosition', + fields=[ + ('RolePositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePositionUUID', primary_key=True, serialize=False)), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), + ('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID')), + ('IsMainPosition', models.SmallIntegerField(db_column='IsMainPosition', default=0)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '角色职务关联', + 'verbose_name_plural': '角色职务关联', + 'db_table': 'RolePosition', + 'managed': True, + }, + ), + migrations.CreateModel( + name='RoleShop', + fields=[ + ('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False)), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), + ('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID')), + ('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '角色商铺关联', + 'verbose_name_plural': '角色商铺关联', + 'db_table': 'RoleShop', + 'managed': True, + }, + ), + migrations.CreateModel( + name='Shop', + fields=[ + ('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False)), + ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')), + ('ShopName', models.CharField(db_column='ShopName', max_length=128)), + ('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64)), + ('ShopType', models.SmallIntegerField(db_column='ShopType', default=0)), + ('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1)), + ('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512)), + ('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128)), + ('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32)), + ('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512)), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)), + ], + options={ + 'verbose_name': '商铺', + 'verbose_name_plural': '商铺', + 'db_table': 'Shop', + 'managed': True, + }, + ), + ] diff --git a/gvsdsdk/migrations/__init__.py b/gvsdsdk/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gvsdsdk/model_base.py b/gvsdsdk/model_base.py new file mode 100644 index 0000000..078f05d --- /dev/null +++ b/gvsdsdk/model_base.py @@ -0,0 +1,351 @@ +from django.db import models +from django.db.models import Q + + +class classproperty: + def __init__(self, func): + self.fget = func + + def __get__(self, obj, objtype=None): + return self.fget(objtype if objtype is not None else type(obj)) + + +ABBREVIATIONS = { + 'uuid': 'UUID', + 'id': 'ID', + 'ip': 'IP', + 'url': 'URL', + 'uri': 'URI', + 'api': 'API', + 'html': 'HTML', + 'css': 'CSS', + 'js': 'JS', + 'json': 'JSON', + 'xml': 'XML', + 'sql': 'SQL', + 'http': 'HTTP', + 'https': 'HTTPS', + 'smtp': 'SMTP', + 'ssl': 'SSL', + 'tls': 'TLS', + 'ssh': 'SSH', + 'ftp': 'FTP', + '2fa': '2FA', + 'elt': 'ELT', + 'sve': 'SVE', + 't4': 'T4', + 'mc': 'MC', + 'crb': 'CRB', + 'ccgo': 'CCGO', + 'ugm': 'UGM', + 'gerp': 'GERP', + 'abac': 'ABAC', + 'rbac': 'RBAC', + 'smtm': 'SMTM', + 'pwd': 'PWD', +} + + +def snake_to_pascal(name): + if '_' not in name: + return name[0].upper() + name[1:] if name else name + parts = name.split('_') + result = [] + for p in parts: + lower = p.lower() + if lower in ABBREVIATIONS: + result.append(ABBREVIATIONS[lower]) + else: + result.append(p.capitalize()) + return ''.join(result) + + +def pascal_to_snake(name): + import re + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) + parts = s2.lower().split('_') + return '_'.join(parts) + + +class _InvertedFieldExpression: + def __init__(self, field_expr): + self._field_expr = field_expr + + def startswith(self, value): + return ~Q(**{f'{self._field_expr._django_name}__startswith': value}) + + def contains(self, value): + return ~Q(**{f'{self._field_expr._django_name}__contains': value}) + + def like(self, pattern): + clean = pattern.strip('%') + return ~Q(**{f'{self._field_expr._django_name}__icontains': clean}) + + def in_(self, values): + return ~Q(**{f'{self._field_expr._django_name}__in': values}) + + def is_(self, other): + if other is None: + return ~Q(**{f'{self._field_expr._django_name}__isnull': True}) + return ~Q(**{self._field_expr._django_name: other}) + + +class FieldExpression: + _resolve_cache = {} + + def __init__(self, model_class, field_name): + self.model_class = model_class + self.field_name = field_name + self._django_name = self._resolve_django_name() + + def _resolve_django_name(self): + cache_key = (self.model_class.__name__, self.field_name) + if cache_key in FieldExpression._resolve_cache: + return FieldExpression._resolve_cache[cache_key] + if hasattr(self.model_class, '_meta'): + for f in self.model_class._meta.local_fields: + if f.name == self.field_name: + FieldExpression._resolve_cache[cache_key] = f.name + return f.name + FieldExpression._resolve_cache[cache_key] = self.field_name + return self.field_name + + def __eq__(self, other): + if isinstance(other, FieldExpression): + return NotImplemented + if other is None: + return Q(**{f'{self._django_name}__isnull': True}) + return Q(**{self._django_name: other}) + + def __ne__(self, other): + if isinstance(other, FieldExpression): + return NotImplemented + return ~Q(**{self._django_name: other}) + + def __gt__(self, other): + return Q(**{f'{self._django_name}__gt': other}) + + def __ge__(self, other): + return Q(**{f'{self._django_name}__gte': other}) + + def __lt__(self, other): + return Q(**{f'{self._django_name}__lt': other}) + + def __le__(self, other): + return Q(**{f'{self._django_name}__lte': other}) + + def __getitem__(self, key): + return _JsonKeyExpression(self._django_name, key) + + def __hash__(self): + return hash((self.model_class.__name__, self.field_name)) + + def __and__(self, other): + if isinstance(other, Q): + return Q(self) & other + return NotImplemented + + def __or__(self, other): + if isinstance(other, Q): + return Q(self) | other + return NotImplemented + + def __invert__(self): + return _InvertedFieldExpression(self) + + def is_(self, other): + if other is None: + return Q(**{f'{self._django_name}__isnull': True}) + return Q(**{self._django_name: other}) + + def is_not(self, other): + if other is None: + return Q(**{f'{self._django_name}__isnull': False}) + return ~Q(**{self._django_name: other}) + + def like(self, pattern): + clean = pattern.strip('%') + return Q(**{f'{self._django_name}__icontains': clean}) + + def contains(self, value): + return Q(**{f'{self._django_name}__contains': value}) + + def ilike(self, pattern): + return self.like(pattern) + + def startswith(self, value): + return Q(**{f'{self._django_name}__startswith': value}) + + def istartswith(self, value): + return Q(**{f'{self._django_name}__istartswith': value}) + + def endswith(self, value): + return Q(**{f'{self._django_name}__endswith': value}) + + def iendswith(self, value): + return Q(**{f'{self._django_name}__iendswith': value}) + + def in_(self, values): + if hasattr(values, '_qs'): + return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)}) + return Q(**{f'{self._django_name}__in': values}) + + def between(self, low, high): + return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high}) + + def regex(self, pattern): + return Q(**{f'{self._django_name}__regex': pattern}) + + def iregex(self, pattern): + return Q(**{f'{self._django_name}__iregex': pattern}) + + def desc(self): + return f'-{self._django_name}' + + def asc(self): + return self._django_name + + def __repr__(self): + return f'FieldExpr({self.model_class.__name__}.{self.field_name})' + + def __str__(self): + return self._django_name + + +class _JsonKeyExpression: + def __init__(self, field_name, key): + self.field_name = field_name + self.key = key + + def _lookup(self, suffix=''): + if suffix: + return f'{self.field_name}__{self.key}__{suffix}' + return f'{self.field_name}__{self.key}' + + def cast(self, type_hint): + return _CastJsonExpression(self.field_name, self.key, type_hint) + + def __eq__(self, other): + return Q(**{self._lookup(): other}) + + def __ne__(self, other): + return ~Q(**{self._lookup(): other}) + + def __gt__(self, other): + return Q(**{self._lookup('gt'): other}) + + def __ge__(self, other): + return Q(**{self._lookup('gte'): other}) + + def __lt__(self, other): + return Q(**{self._lookup('lt'): other}) + + def __le__(self, other): + return Q(**{self._lookup('lte'): other}) + + def between(self, low, high): + return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) + + def __repr__(self): + return f'JsonKeyExpr({self.field_name}[{self.key!r}])' + + +class _CastJsonExpression: + def __init__(self, field_name, key, type_hint): + self.field_name = field_name + self.key = key + self.type_hint = type_hint + + def _lookup(self, suffix=''): + if suffix: + return f'{self.field_name}__{self.key}__{suffix}' + return f'{self.field_name}__{self.key}' + + def between(self, low, high): + return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) + + def __eq__(self, other): + return Q(**{self._lookup(): other}) + + def __ne__(self, other): + return ~Q(**{self._lookup(): other}) + + def __gt__(self, other): + return Q(**{self._lookup('gt'): other}) + + def __ge__(self, other): + return Q(**{self._lookup('gte'): other}) + + def __lt__(self, other): + return Q(**{self._lookup('lt'): other}) + + def __le__(self, other): + return Q(**{self._lookup('lte'): other}) + + def __repr__(self): + type_name = getattr(self.type_hint, '__name__', str(self.type_hint)) + return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})' + + +class ForeignKeyIdDescriptor: + def __init__(self, fk_name): + self.fk_name = fk_name + self.id_attr = fk_name + '_id' + + def __get__(self, obj, objtype=None): + if obj is None: + return FieldExpression(objtype, self.id_attr) + return getattr(obj, self.id_attr) + + def __set__(self, obj, value): + setattr(obj, self.id_attr, value) + + +class FieldExpressionDescriptor: + def __init__(self, field_name): + self.field_name = field_name + + def __get__(self, obj, objtype=None): + if obj is None: + return FieldExpression(objtype, self.field_name) + return getattr(obj, self.field_name) + + def __set__(self, obj, value): + setattr(obj, self.field_name, value) + + +class QModelBase(models.base.ModelBase): + def __new__(mcs, name, bases, namespace, **kwargs): + from django.db import models as dj_models + for attr_name, attr_val in list(namespace.items()): + if isinstance(attr_val, dj_models.Field) and not attr_val.db_column: + # ForeignKey/OneToOneField 的数据库列名是 attr_name + '_id' + if isinstance(attr_val, (dj_models.ForeignKey, dj_models.OneToOneField)): + attr_val.db_column = attr_name + '_id' + else: + attr_val.db_column = attr_name + + # 动态设置 managed:子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表 + meta = namespace.get('Meta') + if meta and getattr(meta, 'app_label', None) == 'gvsdsdk': + try: + from django.conf import settings + if getattr(settings, 'GVSDSDK_MANAGE_TABLES', False): + meta.managed = True + except Exception: + pass + + return super().__new__(mcs, name, bases, namespace, **kwargs) + + +class QModel(models.Model, metaclass=QModelBase): + class Meta: + abstract = True + + @classproperty + def query(cls): + from .fluent import FluentQuery + return FluentQuery(cls) + + diff --git a/gvsdsdk/model_utils.py b/gvsdsdk/model_utils.py new file mode 100644 index 0000000..eee0b9b --- /dev/null +++ b/gvsdsdk/model_utils.py @@ -0,0 +1,59 @@ +import re +import uuid + + +class UUIDObj: + __slots__ = ('_raw',) + + def __init__(self, val): + if val is None: + self._raw = None + elif isinstance(val, UUIDObj): + self._raw = val._raw + elif isinstance(val, uuid.UUID): + self._raw = val + elif isinstance(val, bytes): + self._raw = uuid.UUID(bytes=val) if len(val) == 16 else uuid.UUID(bytes=val[:16]) + elif isinstance(val, str): + self._raw = uuid.UUID(val) + else: + self._raw = uuid.UUID(str(val)) + + @property + def str(self): + if self._raw is None: + return None + return str(self._raw) + + @property + def bytes(self): + if self._raw is None: + return None + return self._raw.bytes + + @property + def uuid(self): + return self._raw + + def __str__(self): + return self.str or '' + + def __repr__(self): + return f'UUIDObj({self.str})' + + def __eq__(self, other): + if isinstance(other, UUIDObj): + return self._raw == other._raw + if isinstance(other, uuid.UUID): + return self._raw == other + if isinstance(other, str): + return self.str == other + if isinstance(other, bytes) and len(other) == 16: + return self.bytes == other + return NotImplemented + + def __hash__(self): + return hash(self._raw) + + def __bool__(self): + return self._raw is not None diff --git a/gvsdsdk/models.py b/gvsdsdk/models.py new file mode 100644 index 0000000..310a117 --- /dev/null +++ b/gvsdsdk/models.py @@ -0,0 +1,813 @@ +"""GVS-DSDK 数据模型模块 + +将 SAAS 主服务器的用户表(User)、权限表(Permission)、角色表(Role)、 +用户角色关联表(UserRole)、角色权限关联表(RolePermission)、 +岗位模板表(PositionTemplate)、岗位模板权限关联表(PositionTemplatePermission)、 +人事职务标签表(Position)、员工部门关联表(UserDept)封装为独立的 Django ORM 模型, +供子服务器直接进行跨服务器 ORM 查询。 + +权限体系四大分层: + 1. 组织人事层:Tenant → Company → Department,员工通过 UserDept 归属部门 + 2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照 + 3. 角色权限载体层:Role + RolePermission,运行时鉴权实体 + 4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性) + +使用方式(子服务器): + from gvsdsdk.models import User, Permission, Role, UserRole, RolePermission + + # 查询所有启用的用户 + users = User.objects.using('saas').filter(UserAccountLicense=1) + + # 或配置 DatabaseRouter 后直接使用 + users = User.objects.filter(UserAccountLicense=1) + +设计原则: + 1. 独立:不依赖 gvsds 内部模块,可在任意 Django 项目中独立使用 + 2. 非托管:managed=False,不创建/迁移表,仅映射已有表结构 + 3. PascalCase:字段名、方法名遵循帕斯卡命名法 + 4. 兼容:db_table、db_column 与 SAAS 主库完全一致 + 5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见 +""" + +import uuid as _uuid +import datetime as _datetime +from django.db import models +from django.contrib.auth.models import BaseUserManager +from gvsdsdk.model_base import QModel +from gvsdsdk.model_utils import UUIDObj + + +# === UUID 字段 === + +class FixedBinaryField(models.BinaryField): + """定长二进制字段:映射 MySQL BINARY(N),可用于主键""" + + description = "Fixed-length binary field" + + def __init__(self, *args, **kwargs): + self.binary_length = kwargs.pop('binary_length', 16) + kwargs.setdefault('max_length', self.binary_length) + kwargs.setdefault('editable', False) + super().__init__(*args, **kwargs) + + def db_type(self, connection): + if connection.vendor == 'mysql': + return f'BINARY({self.binary_length})' + elif connection.vendor == 'sqlite': + return 'BLOB' + elif connection.vendor == 'postgresql': + return 'BYTEA' + return f'BINARY({self.binary_length})' + + def rel_db_type(self, connection): + return self.db_type(connection) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + kwargs['binary_length'] = self.binary_length + return name, path, args, kwargs + + +class UUIDField(FixedBinaryField): + """UUID 字段:数据库存储为 BINARY(16),Python 侧自动转换 str/bytes/uuid.UUID""" + + description = "UUID stored as BINARY(16)" + + def __init__(self, *args, **kwargs): + kwargs.setdefault('max_length', 16) + kwargs.setdefault('editable', False) + super().__init__(*args, **kwargs) + + def db_type(self, connection): + vendor = connection.vendor + if vendor == 'mysql': + return 'BINARY(16)' + elif vendor == 'sqlite': + return 'BLOB' + elif vendor == 'postgresql': + return 'BYTEA' + return 'BINARY(16)' + + def from_db_value(self, value, expression, connection): + if value is None: + return None + if isinstance(value, UUIDObj): + return value + return UUIDObj(value) + + def get_prep_value(self, value): + if value is None: + return None + if isinstance(value, UUIDObj): + return value.bytes + if isinstance(value, str): + try: + return _uuid.UUID(value).bytes + except ValueError: + return None + if isinstance(value, _uuid.UUID): + return value.bytes + if isinstance(value, bytes): + return value + return None + + def get_prep_lookup(self, lookup_type, value): + if lookup_type in ('exact', 'iexact', 'in'): + if value is None: + return None + if lookup_type == 'in' and isinstance(value, (list, tuple)): + return [self.get_prep_value(v) for v in value] + return self.get_prep_value(value) + return super().get_prep_lookup(lookup_type, value) + + def value_to_string(self, obj): + value = self.value_from_object(obj) + if isinstance(value, UUIDObj): + return value.str or '' + if isinstance(value, str): + return value + if isinstance(value, bytes) and len(value) == 16: + return str(_uuid.UUID(bytes=value)) + if isinstance(value, _uuid.UUID): + return str(value) + return '' + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + del kwargs['max_length'] + return name, path, args, kwargs + + +# === QModel 基础模型 === + +# === 用户管理器 === + +class UserManager(BaseUserManager): + """用户管理器——提供用户创建与超级用户创建。 + 方法名保留蛇形命名以兼容 Django 内部约定。""" + + def create_user(self, username, password=None, email='', **extra_fields): + """创建普通用户""" + if not username: + raise ValueError('The Username field must be set') + user = self.model( + UserUUID=_uuid.uuid4().bytes, + UserName=username, + UserEmail=email or '', + **extra_fields + ) + if password: + user.SetPassword(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, password=None, **extra_fields): + """创建超级用户""" + extra_fields.setdefault('UserAccountLicense', 1) + extra_fields.setdefault('UserPositionStatus', 0) + extra_fields.setdefault('IsStaff', True) + extra_fields.setdefault('IsSuperuser', True) + return self.create_user(username, password, **extra_fields) + + +# === 用户表 === +class User(QModel): + """用户表——系统核心身份模型(映射 SAAS 主库 User 表)""" + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') + UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名') + UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码') + UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态') + UserPersonalSignature = models.CharField(max_length=1024, default='', db_column='UserPersonalSignature', verbose_name='个人签名') + UserEmail = models.CharField(max_length=256, default='', db_column='UserEmail', verbose_name='邮箱') + UserEmailVerify = models.SmallIntegerField(default=0, db_column='UserEmailVerify', verbose_name='邮箱验证') + UserAccountLicense = models.SmallIntegerField(default=1, db_column='UserAccountLicense', verbose_name='账户许可') + UserGender = models.SmallIntegerField(default=-1, db_column='UserGender', verbose_name='性别') + UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话') + UserCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserCreateTime', verbose_name='创建时间') + UserLastLoginDate = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserLastLoginDate', verbose_name='最后登录') + IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工') + IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户') + + objects = UserManager() + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'User' + verbose_name = '用户' + verbose_name_plural = '用户' + + def __str__(self): + return self.UserName + + @property + def UUIDStr(self): + """返回 UUID 字符串形式""" + return UUIDObj(self.UserUUID).str + + @property + def IsActive(self): + """账户是否启用(UserAccountLicense == 1)""" + return self.UserAccountLicense == 1 + + @property + def IsOnJob(self): + """是否在职(UserPositionStatus == 0)""" + return self.UserPositionStatus == 0 + + @property + def IsAnonymous(self): + return False + + @property + def IsAuthenticated(self): + return True + + @property + def PK(self): + return UUIDObj(self.UserUUID) + + @property + def ID(self): + return UUIDObj(self.UserUUID) + + def SetPassword(self, RawPassword): + """设置密码——bcrypt 哈希""" + import bcrypt + salt = bcrypt.gensalt(rounds=12) + hashed = bcrypt.hashpw(RawPassword.encode('utf-8'), salt) + self.UserPassword = hashed.decode('utf-8') + + def CheckPassword(self, RawPassword): + """验证密码""" + import bcrypt + try: + return bcrypt.checkpw( + RawPassword.encode('utf-8'), + self.UserPassword.encode('utf-8') + ) + except Exception: + return False + + def GetTele(self): + """获取电话,空则返回默认文本""" + return self.UserTele or '未填写' + + def GetCompanies(self): + """获取用户所属公司列表""" + # Company 模型不在 gvsdsdk 中,此处返回空 QuerySet + # 如需使用,请在子服务器中自行实现 + return [] + + def GetRoles(self): + """获取用户角色列表(需在同一 database 连接下使用)""" + user_role_uuids = UserRole.objects.filter( + UserUUID=self.UserUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=user_role_uuids) + + def GetDepartments(self): + """获取用户部门列表""" + # Department 模型不在 gvsdsdk 中,此处返回空列表 + # 如需使用,请在子服务器中自行实现 + return [] + + def HasPermission(self, PermCode): + """检查用户是否拥有指定权限(需在同一 database 连接下使用) + + 通过 UserRole → RolePermission → Permission 链路查询, + 汇总所有角色的操作权限后判断。 + """ + user_role_uuids = UserRole.objects.filter( + UserUUID=self.UserUUID + ).values_list('RoleUUID', flat=True) + perm_uuids = RolePermission.objects.filter( + RoleUUID__in=user_role_uuids + ).values_list('PermUUID', flat=True) + return Permission.objects.filter( + PermUUID__in=perm_uuids, + PermCode=PermCode, + PermStatus=1, + ).exists() + + # Django auth 框架兼容(内部使用蛇形命名) + @property + def is_active(self): + return self.IsActive + + @property + def is_anonymous(self): + return self.IsAnonymous + + @property + def is_authenticated(self): + return self.IsAuthenticated + + @property + def pk(self): + return self.PK + + @property + def id(self): + return self.ID + + def set_password(self, raw_password): + self.SetPassword(raw_password) + + def check_password(self, raw_password): + return self.CheckPassword(raw_password) + + +# === 用户头像表 === + +class UserAvatar(QModel): + """用户头像表(映射 SAAS 主库 UserAvatar 表)""" + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') + UserAvatar = models.BinaryField(editable=True, db_column='UserAvatar', verbose_name='头像数据') + AvatarUploadTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='AvatarUploadTime', verbose_name='上传时间') + AvatarFileType = models.CharField(max_length=16, default='png', db_column='AvatarFileType', verbose_name='文件类型') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserAvatar' + verbose_name = '用户头像' + verbose_name_plural = '用户头像' + + +# === 用户双因素认证表 === + +class User2FA(QModel): + """用户双因素认证表(映射 SAAS 主库 User2FA 表)""" + + User2FAUUID = UUIDField(primary_key=True, db_column='User2FAUUID', verbose_name='2FA UUID') + UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') + SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') + IsEnabled = models.SmallIntegerField(default=0, db_column='IsEnabled', verbose_name='是否启用') + Verified = models.SmallIntegerField(default=0, db_column='Verified', verbose_name='已验证') + LastUsedAt = models.DateTimeField(null=True, blank=True, db_column='LastUsedAt', verbose_name='最后使用') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'User2FA' + verbose_name = '双因素认证' + verbose_name_plural = '双因素认证' + + +# === 双因素认证待验证表 === + +class UserPending2FA(QModel): + """双因素认证待验证表(映射 SAAS 主库 UserPending2FA 表)""" + + PendingUUID = UUIDField(primary_key=True, db_column='PendingUUID', verbose_name='待确认UUID') + UserUUID = UUIDField(unique=True, db_column='UserUUID', verbose_name='用户UUID') + SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') + ExpireAt = models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserPending2FA' + verbose_name = '2FA待确认' + verbose_name_plural = '2FA待确认' + + +# === 临时令牌表 === + +def _EltEndTimeDefault(): + from django.conf import settings + days = getattr(settings, 'ELT_TOKEN_LIFETIME_DAYS', 30) + return _datetime.datetime.utcnow() + _datetime.timedelta(days=days) + + +class EphemeralToken(QModel): + """临时令牌表——用于 API 认证与会话管理(映射 SAAS 主库 EphemeralToken 表)""" + + ELTUUID = UUIDField(primary_key=True, db_column='ELTUUID', verbose_name='令牌UUID') + ELTUserUUID = UUIDField(db_index=True, db_column='ELTUserUUID', verbose_name='用户UUID') + ELTCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='ELTCreateTime', verbose_name='创建时间') + ELTEndTime = models.DateTimeField(default=_EltEndTimeDefault, db_column='ELTEndTime', verbose_name='过期时间') + ELTType = models.SmallIntegerField(default=0, db_column='ELTType', verbose_name='令牌类型') + ELTAllowIP = models.JSONField(default=list, db_column='ELTAllowIP', verbose_name='允许IP') + ELTStatus = models.SmallIntegerField(default=1, db_index=True, db_column='ELTStatus', verbose_name='状态') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'EphemeralToken' + verbose_name = '临时令牌' + verbose_name_plural = '临时令牌' + + def IsExpired(self): + """检查令牌是否过期""" + return _datetime.datetime.utcnow() > self.ELTEndTime.replace(tzinfo=None) + + def IsValid(self): + """检查令牌是否有效""" + return self.ELTStatus == 1 and not self.IsExpired() + + +# === 用户登录日志表 === + +class UserLoginLog(QModel): + """用户登录日志表(映射 SAAS 主库 UserLoginLog 表)""" + + LogUUID = UUIDField(primary_key=True, db_column='LogUUID', verbose_name='日志UUID') + UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') + LoginIP = models.CharField(max_length=39, db_column='LoginIP', verbose_name='登录IP') + LoginPlatform = models.CharField(max_length=32, db_column='LoginPlatform', verbose_name='登录平台') + LoginStatus = models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态') + LoginTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='LoginTime', verbose_name='登录时间') + UserAgent = models.CharField(max_length=512, default='', db_column='UserAgent', verbose_name='用户代理') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserLoginLog' + verbose_name = '登录日志' + verbose_name_plural = '登录日志' + + +# === 账户激活码表 === + +class AccountActivation(QModel): + """账户激活码表(映射 SAAS 主库 AccountActivation 表)""" + + AccountActivationCode = FixedBinaryField(primary_key=True, binary_length=6, db_column='AccountActivationCode', verbose_name='激活码') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'AccountActivation' + verbose_name = '账户激活' + verbose_name_plural = '账户激活' + + +# === 角色表 === + +class Role(QModel): + """角色表——权限分配单元(映射 SAAS 主库 Role 表) + + 角色是真正承载操作权限的实体,在单个租户内部独立存在, + 不依附任何公司或部门。通过 AssignScope 控制角色的分配可见范围: + - 0:全租户管理员可分发 + - 1:仅指定公司管理员可分发(AssignScopeUUID = CompanyUUID) + - 2:仅指定部门管理员可分发(AssignScopeUUID = DeptUUID) + """ + + RoleUUID = UUIDField(primary_key=True, db_column='RoleUUID', verbose_name='角色UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + RoleName = models.CharField(max_length=128, db_column='RoleName', verbose_name='角色名称') + RoleType = models.SmallIntegerField(default=2, db_column='RoleType', verbose_name='角色类型') + RoleStatus = models.SmallIntegerField(default=1, db_column='RoleStatus', verbose_name='角色状态') + AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围') + AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID') + RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Role' + verbose_name = '角色' + verbose_name_plural = '角色' + + def __str__(self): + return self.RoleName + + def GetPermissions(self): + """获取角色拥有的权限列表(通过 RolePermission 中间表)""" + perm_uuids = RolePermission.objects.filter( + RoleUUID=self.RoleUUID + ).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + def GetUsers(self): + """获取拥有该角色的用户列表""" + user_uuids = UserRole.objects.filter( + RoleUUID=self.RoleUUID + ).values_list('UserUUID', flat=True) + return User.objects.filter(UserUUID__in=user_uuids) + + +# === 权限表 === + +class Permission(QModel): + """权限表——操作权限定义(映射 SAAS 主库 Permission 表) + + 独立的权限定义表,租户内全局可用,不再挂在角色下。 + 通过 RolePermission 中间表与角色建立多对多关系, + 通过 PositionTemplatePermission 中间表与岗位模板建立多对多关系。 + """ + + PermUUID = UUIDField(primary_key=True, db_column='PermUUID', verbose_name='权限UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + PermName = models.CharField(max_length=128, db_column='PermName', verbose_name='权限名称') + PermCode = models.CharField(max_length=128, db_column='PermCode', verbose_name='权限键') + PermCategory = models.CharField(max_length=64, default='', db_column='PermCategory', verbose_name='权限分类') + PermStatus = models.SmallIntegerField(default=1, db_column='PermStatus', verbose_name='是否启用') + PermDesc = models.CharField(max_length=512, default='', db_column='PermDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Permission' + verbose_name = '权限' + verbose_name_plural = '权限' + + def __str__(self): + return f"{self.PermName} ({self.PermCode})" + + def GetRoles(self): + """获取拥有该权限的角色列表""" + role_uuids = RolePermission.objects.filter( + PermUUID=self.PermUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + +# === 用户角色关联表 === + +class UserRole(QModel): + """用户角色关联表——用户与角色的多对多映射(映射 SAAS 主库 UserRole 表) + + 通过 CompanyUUID 标识该用户-角色关联所属的公司, + 用于菜单按公司差异化显示、数据权限按公司过滤等场景。 + 同一用户在不同公司下可绑定不同角色。 + + 注意:UserUUID 和 RoleUUID 在 gvsds 中是 ForeignKey, + 但此处改为 UUIDField 以保持独立性。 + """ + + UserRoleUUID = UUIDField(primary_key=True, db_column='UserRoleUUID', verbose_name='关联UUID') + # 原 gvsds 中为 ForeignKey(User),此处改为 UUIDField + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + # 原 gvsds 中为 ForeignKey('Role'),此处改为 UUIDField + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserRole' + verbose_name = '用户角色关联' + verbose_name_plural = '用户角色关联' + + +# === 角色权限关联表 === + +class RolePermission(QModel): + """角色权限关联表——角色与操作权限的多对多映射(映射 SAAS 主库 RolePermission 表) + + 一个角色可以拥有多个权限,一个权限也可以被多个角色引用。 + 角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。 + """ + + RolePermUUID = UUIDField(primary_key=True, db_column='RolePermUUID', verbose_name='关联UUID') + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'RolePermission' + verbose_name = '角色权限关联' + verbose_name_plural = '角色权限关联' + + +# === 用户 ABAC 表 === + +class UserABAC(QModel): + """用户 ABAC 表——基于属性的访问控制(映射 SAAS 主库 UserABAC 表)""" + + UserABACUUID = UUIDField(primary_key=True, db_column='UserABACUUID', verbose_name='ABAC UUID') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + HierarchicalPermissionIdentifiers = models.CharField(max_length=256, db_column='HierarchicalPermissionIdentifiers', verbose_name='层级权限标识') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserABAC' + verbose_name = '用户ABAC' + verbose_name_plural = '用户ABAC' + + +# === 密码表 === + +class Password(QModel): + """密码表——独立密码存储(映射 SAAS 主库 Password 表)""" + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') + Password = models.CharField(max_length=1024, db_column='Password', verbose_name='密码哈希') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Password' + verbose_name = '密码' + verbose_name_plural = '密码' + + +# === 商铺表 === + +class Shop(QModel): + """商铺表——SAAS 多商铺体系中的商铺实体 + + 每个商铺属于一个公司(CompanyUUID),可关联多个角色(通过 RoleShop)。 + """ + + ShopUUID = UUIDField(primary_key=True, db_column='ShopUUID', verbose_name='商铺UUID') + CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') + ShopName = models.CharField(max_length=128, db_column='ShopName', verbose_name='商铺名称') + ShopCode = models.CharField(max_length=64, default='', db_column='ShopCode', verbose_name='商铺编码') + ShopType = models.SmallIntegerField(default=0, db_column='ShopType', verbose_name='商铺类型') + ShopStatus = models.SmallIntegerField(default=1, db_column='ShopStatus', verbose_name='商铺状态') + ShopAddress = models.CharField(max_length=512, default='', db_column='ShopAddress', verbose_name='商铺地址') + ShopContact = models.CharField(max_length=128, default='', db_column='ShopContact', verbose_name='联系人') + ShopPhone = models.CharField(max_length=32, default='', db_column='ShopPhone', verbose_name='联系电话') + ShopDesc = models.CharField(max_length=512, default='', db_column='ShopDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Shop' + verbose_name = '商铺' + verbose_name_plural = '商铺' + + def __str__(self): + return self.ShopName + + def GetRoles(self): + """获取商铺关联的所有角色""" + role_uuids = RoleShop.objects.filter( + ShopUUID=self.ShopUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + +# === 角色商铺关联表 === + +class RoleShop(QModel): + """角色商铺关联表——角色与商铺的多对多映射 + + 一个角色可以管理多个商铺,一个商铺也可以被多个角色管理。 + """ + + RoleShopUUID = UUIDField(primary_key=True, db_column='RoleShopUUID', verbose_name='关联UUID') + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + ShopUUID = UUIDField(db_column='ShopUUID', verbose_name='商铺UUID') + IsMainShop = models.SmallIntegerField(default=0, db_column='IsMainShop', verbose_name='主商铺') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'RoleShop' + verbose_name = '角色商铺关联' + verbose_name_plural = '角色商铺关联' + + +# === 岗位模板表 === + +class PositionTemplate(QModel): + """岗位模板表——权限初始化快照模板(映射 SAAS 主库 PositionTemplate 表) + + 租户内全局可重复使用,定位是权限初始化快照模板, + 不直接分配给员工、不参与运行时鉴权。 + 新建角色时可一键复制模板内的全部权限内容生成新角色。 + 模板后续调整只会作用于之后新创建的角色, + 已有角色不会同步变更,实现模板复用和存量权限独立管控互不干扰。 + """ + + PositionTemplateUUID = UUIDField(primary_key=True, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + TemplateName = models.CharField(max_length=128, db_column='TemplateName', verbose_name='模板名称') + TemplateCode = models.CharField(max_length=64, default='', db_column='TemplateCode', verbose_name='模板编码') + TemplateStatus = models.SmallIntegerField(default=1, db_column='TemplateStatus', verbose_name='模板状态') + TemplateDesc = models.CharField(max_length=512, default='', db_column='TemplateDesc', verbose_name='模板描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'PositionTemplate' + verbose_name = '岗位模板' + verbose_name_plural = '岗位模板' + + def __str__(self): + return self.TemplateName + + def GetPermissions(self): + """获取模板关联的权限列表""" + perm_uuids = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=self.PositionTemplateUUID + ).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + def CopyToRole(self, RoleName, TenantUUID): + """从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色)""" + role = Role.objects.create( + RoleUUID=_uuid.uuid4().bytes, + TenantUUID=TenantUUID, + RoleName=RoleName, + ) + template_perms = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=self.PositionTemplateUUID + ) + for tp in template_perms: + RolePermission.objects.create( + RolePermUUID=_uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=tp.PermUUID, + ) + return role + + +# === 岗位模板权限关联表 === + +class PositionTemplatePermission(QModel): + """岗位模板权限关联表——岗位模板与操作权限的多对多映射 + + 仅作为复制源头,不参与运行时鉴权。 + 新建角色时一键复制模板权限到角色,复制后模板与角色无运行时绑定。 + """ + + PTPermUUID = UUIDField(primary_key=True, db_column='PTPermUUID', verbose_name='关联UUID') + PositionTemplateUUID = UUIDField(db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') + PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'PositionTemplatePermission' + verbose_name = '岗位模板权限关联' + verbose_name_plural = '岗位模板权限关联' + + +# === 人事职务标签表 === + +class Position(QModel): + """人事职务标签表——仅用于人事管理标注(映射 SAAS 主库 Position 表) + + 员工会标注对应的职务标签,该标签仅为人事标注信息, + 不直接绑定权限,和角色、权限体系完全解耦。 + 人员调岗、升职只修改组织归属和人事职务标签,权限只需增删绑定角色即可,互不干扰。 + """ + + PositionUUID = UUIDField(primary_key=True, db_column='PositionUUID', verbose_name='职务UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + PositionName = models.CharField(max_length=128, db_column='PositionName', verbose_name='职务名称') + PositionCode = models.CharField(max_length=64, default='', db_column='PositionCode', verbose_name='职务编码') + PositionLevel = models.SmallIntegerField(default=0, db_column='PositionLevel', verbose_name='职务级别') + PositionStatus = models.SmallIntegerField(default=1, db_column='PositionStatus', verbose_name='状态') + PositionDesc = models.CharField(max_length=512, default='', db_column='PositionDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'Position' + verbose_name = '人事职务标签' + verbose_name_plural = '人事职务标签' + + def __str__(self): + return self.PositionName + + +# === 员工部门关联表 === + +class UserDept(QModel): + """员工部门关联表——员工与部门的多对多映射(映射 SAAS 主库 UserDept 表) + + 支持一人多部门兼职,同时标注该员工在对应部门的职务标签。 + 该层级仅用于组织架构展示、审批流转、人员统计, + 同时为数据权限判断提供人员所属组织属性依据,不参与操作权限校验。 + """ + + UserDeptUUID = UUIDField(primary_key=True, db_column='UserDeptUUID', verbose_name='关联UUID') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID') + PositionUUID = UUIDField(null=True, blank=True, db_column='PositionUUID', verbose_name='职务标签UUID') + IsMainDept = models.SmallIntegerField(default=1, db_column='IsMainDept', verbose_name='是否主部门') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = False + app_label = 'gvsdsdk' + db_table = 'UserDept' + verbose_name = '员工部门关联' + verbose_name_plural = '员工部门关联' diff --git a/gvsdsdk/msync/__init__.py b/gvsdsdk/msync/__init__.py new file mode 100644 index 0000000..887ae50 --- /dev/null +++ b/gvsdsdk/msync/__init__.py @@ -0,0 +1,73 @@ +"""GVS-DSDK MSYNC 子包 + +将 MSYNC 相关模块统一组织在 gvsdsdk.msync 命名空间下。 + +自有实现(无 Django 依赖,子服务器可直接使用): + - auth: HMAC-SHA256 签名验证、服务令牌编解码 + - client: 微服务通信客户端(REST) + - local_tx: 本地事务步骤处理器 + +服务端转发(依赖 Django ORM,仅在主服务器可用): + - registry: 服务注册/发现 + - ogm: 对象全局映射 + - transaction: Saga 分布式事务 +""" + +# 自有实现(无 Django 依赖,始终可用) +from gvsdsdk.msync.auth import ( # noqa + SignPayload, + VerifySignature, + GenerateChallenge, + BuildAuthPayload, + SignAuthPayload, + VerifyAuthPayload, + ServiceTokenEncoder, +) +from gvsdsdk.msync.client import ServiceClient, ServiceClientError # noqa +from gvsdsdk.msync.local_tx import LocalTransactionManager, StepHandler, local_tx # noqa + +# 服务端转发(Django ORM 依赖,延迟导入避免子服务器初始化报错) +def __getattr__(name): + _server_exports = { + 'ServiceRegistry': 'gvsdsdk.msync.registry', + 'RegistryError': 'gvsdsdk.msync.registry', + 'OGMManager': 'gvsdsdk.msync.ogm', + 'OGMError': 'gvsdsdk.msync.ogm', + 'TransactionManager': 'gvsdsdk.msync.transaction', + 'TransactionError': 'gvsdsdk.msync.transaction', + 'StepExecutionError': 'gvsdsdk.msync.transaction', + } + if name in _server_exports: + import importlib + mod = importlib.import_module(_server_exports[name]) + return getattr(mod, name) + raise AttributeError(f'module {__name__!r} has no attribute {name!r}') + + +__all__ = [ + # auth (自有) + 'SignPayload', + 'VerifySignature', + 'GenerateChallenge', + 'BuildAuthPayload', + 'SignAuthPayload', + 'VerifyAuthPayload', + 'ServiceTokenEncoder', + # client (自有) + 'ServiceClient', + 'ServiceClientError', + # local_tx (自有) + 'LocalTransactionManager', + 'StepHandler', + 'local_tx', + # registry (服务端转发) + 'ServiceRegistry', + 'RegistryError', + # ogm (服务端转发) + 'OGMManager', + 'OGMError', + # transaction (服务端转发) + 'TransactionManager', + 'TransactionError', + 'StepExecutionError', +] diff --git a/gvsdsdk/msync/auth.py b/gvsdsdk/msync/auth.py new file mode 100644 index 0000000..e4c02cf --- /dev/null +++ b/gvsdsdk/msync/auth.py @@ -0,0 +1,130 @@ +"""MSYNC 认证模块 + +提供 HMAC-SHA256 签名验证、服务令牌编解码等纯函数。 +Django ORM 依赖的函数(AuthenticateService, IssueServiceToken 等) +仍在 gvsds.apps.msync.auth 中,由服务端调用。 +""" + +import hmac +import hashlib +import time +import json +import base64 +import uuid +import logging +import datetime + +logger = logging.getLogger(__name__) + + +def SignPayload(Payload, Secret): + """使用 HMAC-SHA256 对载荷签名""" + if isinstance(Payload, dict): + Payload = json.dumps(Payload, sort_keys=True, separators=(',', ':')) + if isinstance(Payload, str): + Payload = Payload.encode('utf-8') + return hmac.new(Secret.encode('utf-8'), Payload, hashlib.sha256).hexdigest() + + +def VerifySignature(Payload, Signature, Secret): + """验证 HMAC-SHA256 签名""" + Expected = SignPayload(Payload, Secret) + return hmac.compare_digest(Expected, Signature) + + +def GenerateChallenge(): + """生成认证挑战字符串(时间戳 + 随机数)""" + return f'{time.time():.6f}:{uuid.uuid4().hex}' + + +def BuildAuthPayload(ServiceName, Challenge, Timestamp=None): + """构建认证请求载荷""" + if Timestamp is None: + Timestamp = int(time.time()) + return { + 'service': ServiceName, + 'challenge': Challenge, + 'ts': Timestamp, + } + + +def SignAuthPayload(Payload, ServiceSecret): + """对认证载荷签名""" + Canonical = json.dumps(Payload, sort_keys=True, separators=(',', ':')) + return SignPayload(Canonical, ServiceSecret) + + +def VerifyAuthPayload(Payload, Signature, ServiceSecret): + """验证认证载荷签名""" + Canonical = json.dumps(Payload, sort_keys=True, separators=(',', ':')) + return VerifySignature(Canonical, Signature, ServiceSecret) + + +class ServiceTokenEncoder: + """服务令牌编解码器——自包含令牌,无需数据库查询即可验证""" + + ALGORITHM = 'HS256' + ISSUER = 'MSYNC' + + def __init__(self, SecretKey=None): + self.SecretKey = SecretKey or '' + + def Encode(self, service_name=None, scope=None, expires_at=None, master_secret=None, **kwargs): + """编码令牌 + + 支持两种调用方式: + 1. 位置参数: Encode(payload_dict) — payload 包含所有字段 + 2. 关键字参数: Encode(service_name=..., scope=..., expires_at=..., master_secret=...) + """ + # 如果传入的是字典作为第一个位置参数 + if service_name is not None and isinstance(service_name, dict) and scope is None: + Payload = service_name + Secret = master_secret or self.SecretKey + else: + Secret = master_secret or self.SecretKey + Payload = { + 'iss': self.ISSUER, + 'sub': service_name, + 'scope': scope or [], + } + if expires_at is not None: + if hasattr(expires_at, 'timestamp'): + Payload['exp'] = int(expires_at.timestamp()) + else: + Payload['exp'] = int(expires_at) + + Header = json.dumps({'alg': self.ALGORITHM, 'typ': 'JWT'}) + PayloadData = json.dumps(Payload, sort_keys=True, separators=(',', ':'), default=str) + HeaderB64 = base64.urlsafe_b64encode(Header.encode()).rstrip(b'=').decode() + PayloadB64 = base64.urlsafe_b64encode(PayloadData.encode()).rstrip(b'=').decode() + SigningInput = f'{HeaderB64}.{PayloadB64}' + Signature = hmac.new( + Secret.encode('utf-8'), + SigningInput.encode('utf-8'), + hashlib.sha256, + ).hexdigest() + return f'{SigningInput}.{Signature}' + + def Decode(self, Token, SecretKey=None): + """解码并验证令牌""" + Secret = SecretKey or self.SecretKey + try: + Parts = Token.split('.') + if len(Parts) != 3: + return None + HeaderB64, PayloadB64, Signature = Parts + SigningInput = f'{HeaderB64}.{PayloadB64}' + ExpectedSig = hmac.new( + Secret.encode('utf-8'), + SigningInput.encode('utf-8'), + hashlib.sha256, + ).hexdigest() + if not hmac.compare_digest(ExpectedSig, Signature): + return None + Padding = 4 - len(PayloadB64) % 4 + if Padding != 4: + PayloadB64 += '=' * Padding + PayloadJSON = base64.urlsafe_b64decode(PayloadB64).decode('utf-8') + return json.loads(PayloadJSON) + except Exception: + return None diff --git a/gvsdsdk/msync/client.py b/gvsdsdk/msync/client.py new file mode 100644 index 0000000..21ffe5d --- /dev/null +++ b/gvsdsdk/msync/client.py @@ -0,0 +1,275 @@ +"""MSYNC 服务客户端模块 + +提供 ServiceClient 类,用于微服务间发起认证请求。 +支持自动令牌获取与刷新、OGM 解析、服务发现。 +本模块自包含实现(使用 urllib),不依赖 Django。 +""" + +import json +import time +import logging +import urllib.request +import urllib.error + +logger = logging.getLogger(__name__) + + +MSYNC_AUTH_PATH = '/api/msync/auth/token/' +MSYNC_AUTH_REFRESH_PATH = '/api/msync/auth/token/refresh/' +MSYNC_OGM_RESOLVE_PATH = '/api/msync/ogm/{global_id}/' +MSYNC_REGISTRY_PATH = '/api/msync/registry/{name}/' + +from gvsdsdk.msync.auth import SignPayload, BuildAuthPayload + + +def _GenerateChallenge(): + return f'{int(time.time() * 1000)}-{id(object())}' + + +class ServiceClientError(Exception): + def __init__(self, Message, StatusCode=None, ResponseBody=None): + super().__init__(Message) + self.StatusCode = StatusCode + self.ResponseBody = ResponseBody + + +class _TokenStore: + """内存中的令牌存储""" + + def __init__(self): + self._AccessToken = None + self._RefreshToken = None + self._ExpiresAt = 0.0 + self._Scope = [] + + @property + def AccessToken(self): + return self._AccessToken + + @property + def RefreshToken(self): + return self._RefreshToken + + @property + def Scope(self): + return self._Scope + + def IsValid(self): + return self._AccessToken is not None and time.time() < self._ExpiresAt + + def Store(self, AccessToken, RefreshToken, ExpiresAt, Scope=None): + self._AccessToken = AccessToken + self._RefreshToken = RefreshToken + self._ExpiresAt = ExpiresAt + self._Scope = Scope or [] + + def Clear(self): + self._AccessToken = None + self._RefreshToken = None + self._ExpiresAt = 0.0 + self._Scope = [] + + +class ServiceClient: + """微服务通信客户端——用于向其他服务发起认证请求 + + 用法: + client = ServiceClient( + ServiceName='gerp', + ServiceSecret='your-secret', + ) + client.Authenticate() + + # 获取其他服务信息 + info = client.DiscoverService('crb') + + # 解析 OGM + ogm = client.ResolveOGM('user:abc123') + + # 向其他服务发请求 + response = client.Request('GET', 'https://crb.gvsds.com/api/wallet', Scope=['ledger:read']) + """ + + def __init__(self, ServiceName, ServiceSecret, MasterURL=None): + self.ServiceName = ServiceName + self.ServiceSecret = ServiceSecret + if MasterURL is None: + MasterURL = 'https://api.gvsds.com' + self.MasterURL = MasterURL.rstrip('/') + self._tokens = _TokenStore() + + @property + def IsAuthenticated(self): + return self._tokens.IsValid() + + @property + def AccessToken(self): + return self._tokens.AccessToken + + @property + def RefreshToken(self): + return self._tokens.RefreshToken + + def Authenticate(self): + if not self._Authenticate(): + raise ServiceClientError('认证失败') + return self + + def Refresh(self): + if not self._RefreshToken(): + raise ServiceClientError('刷新失败') + return self + + def _Authenticate(self): + challenge = _GenerateChallenge() + payload = BuildAuthPayload(self.ServiceName, challenge) + signature = SignPayload(payload, self.ServiceSecret) + + req_body = json.dumps({ + 'ServiceName': self.ServiceName, + 'Payload': payload, + 'Signature': signature, + }).encode('utf-8') + + req = urllib.request.Request( + f'{self.MasterURL}{MSYNC_AUTH_PATH}', + data=req_body, + headers={'Content-Type': 'application/json'}, + method='POST', + ) + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode('utf-8')) + except urllib.error.HTTPError as e: + body = e.read().decode('utf-8', errors='replace') + logger.error(f'认证失败 HTTP {e.code}: {body}') + return False + except Exception as e: + logger.error(f'认证异常: {e}') + return False + + if data.get('code') not in (200, 0): + logger.error(f'认证业务错误: {data}') + return False + + token_data = data.get('data') or {} + self._tokens.Store( + AccessToken=token_data.get('AccessToken'), + RefreshToken=token_data.get('RefreshToken'), + ExpiresAt=token_data.get('ExpiresAt', 0), + Scope=token_data.get('Scope', []), + ) + return self._tokens.AccessToken is not None + + def _RefreshToken(self): + if not self._tokens.RefreshToken: + return False + + req_body = json.dumps({ + 'ServiceName': self.ServiceName, + 'RefreshToken': self._tokens.RefreshToken, + }).encode('utf-8') + + req = urllib.request.Request( + f'{self.MasterURL}{MSYNC_AUTH_REFRESH_PATH}', + data=req_body, + headers={'Content-Type': 'application/json'}, + method='POST', + ) + + try: + with urllib.request.urlopen(req, timeout=30) as resp: + data = json.loads(resp.read().decode('utf-8')) + except Exception as e: + logger.error(f'刷新令牌异常: {e}') + return False + + if data.get('code') not in (200, 0): + return False + + token_data = data.get('data') or {} + self._tokens.Store( + AccessToken=token_data.get('AccessToken'), + RefreshToken=token_data.get('RefreshToken'), + ExpiresAt=token_data.get('ExpiresAt', 0), + Scope=token_data.get('Scope', []), + ) + return self._tokens.AccessToken is not None + + def _EnsureAuthenticated(self): + if self.IsAuthenticated: + return + if self._tokens.RefreshToken: + self._RefreshToken() + return + self._Authenticate() + + def Request(self, Method, URL, Data=None, Headers=None, Scope=None, Timeout=30): + self._EnsureAuthenticated() + + req_headers = { + 'Authorization': f'Service {self.AccessToken}', + 'Content-Type': 'application/json', + 'X-Source-Service': self.ServiceName, + } + + if Scope: + req_headers['X-Required-Scope'] = ','.join(Scope) + + if Headers: + req_headers.update(Headers) + + body = None + if Data is not None: + if isinstance(Data, dict): + body = json.dumps(Data).encode('utf-8') + elif isinstance(Data, bytes): + body = Data + elif isinstance(Data, str): + body = Data.encode('utf-8') + + req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method) + + try: + with urllib.request.urlopen(req, timeout=Timeout) as resp: + response_data = resp.read().decode('utf-8') + content_type = resp.headers.get('Content-Type', '') + if 'application/json' in content_type: + return json.loads(response_data) + return response_data + except urllib.error.HTTPError as e: + body_text = e.read().decode('utf-8', errors='replace') + if e.code == 401: + logger.info('令牌可能已失效,尝试刷新') + self._tokens.Clear() + self._EnsureAuthenticated() + req_headers['Authorization'] = f'Service {self.AccessToken}' + retry_req = urllib.request.Request(URL, data=body, headers=req_headers, method=Method) + try: + with urllib.request.urlopen(retry_req, timeout=Timeout) as resp2: + response_data = resp2.read().decode('utf-8') + content_type = resp2.headers.get('Content-Type', '') + if 'application/json' in content_type: + return json.loads(response_data) + return response_data + except urllib.error.HTTPError as e2: + body2 = e2.read().decode('utf-8', errors='replace') + raise ServiceClientError( + f'重试后仍失败: HTTP {e2.code}', + StatusCode=e2.code, + ResponseBody=body2, + ) + raise ServiceClientError( + f'请求失败: HTTP {e.code}', + StatusCode=e.code, + ResponseBody=body_text, + ) + + def ResolveOGM(self, GlobalID): + url = f'{self.MasterURL}{MSYNC_OGM_RESOLVE_PATH.format(global_id=GlobalID)}' + return self.Request('GET', url, Scope=['ogm:read']) + + def DiscoverService(self, ServiceName): + url = f'{self.MasterURL}{MSYNC_REGISTRY_PATH.format(name=ServiceName)}' + return self.Request('GET', url, Scope=['registry:read']) diff --git a/gvsdsdk/msync/local_tx.py b/gvsdsdk/msync/local_tx.py new file mode 100644 index 0000000..0496ad4 --- /dev/null +++ b/gvsdsdk/msync/local_tx.py @@ -0,0 +1,105 @@ +"""本地事务管理器 + +每个工作服务运行一个 LocalTransactionManager 实例, +负责注册本服务的步骤处理器(action + compensate), +并在收到主服务器请求时执行本地逻辑。 + +用法(在服务启动时注册处理器): + from gvsdsdk.msync.local_tx import local_tx + + local_tx.register( + 'create_user', + action=create_user_action, + compensate=create_user_compensate, + ) +""" + +import logging +import traceback + +logger = logging.getLogger(__name__) + + +class StepHandler: + """步骤处理器——封装 action 与 compensate 函数""" + + def __init__(self, name, action, compensate=None): + self.Name = name + self.Action = action + self.Compensate = compensate + + def __repr__(self): + return f'' + + +class LocalTransactionManager: + """本地事务管理器——每个工作服务一个实例""" + + def __init__(self): + self._handlers = {} + + def register(self, name, action, compensate=None): + """注册步骤处理器""" + if name in self._handlers: + logger.warning(f'步骤处理器已存在,覆盖: {name}') + self._handlers[name] = StepHandler( + name=name, + action=action, + compensate=compensate, + ) + logger.info(f'步骤处理器注册: {name}') + + def unregister(self, name): + """注销步骤处理器""" + if name in self._handlers: + del self._handlers[name] + logger.info(f'步骤处理器注销: {name}') + + def has_handler(self, name): + """检查是否注册了指定步骤处理器""" + return name in self._handlers + + def list_handlers(self): + """列出所有已注册的步骤处理器名称""" + return list(self._handlers.keys()) + + def execute_step(self, name, payload=None): + """执行步骤——调用本地 action 函数""" + handler = self._handlers.get(name) + if handler is None: + msg = f'未注册的步骤处理器: {name}' + logger.error(msg) + return {'success': False, 'result': None, 'error': msg} + + try: + result = handler.Action(payload or {}) + logger.info(f'步骤执行成功: {name}') + return {'success': True, 'result': result, 'error': None} + except Exception as e: + msg = f'步骤执行失败: {name}: {e}\n{traceback.format_exc()}' + logger.error(msg) + return {'success': False, 'result': None, 'error': str(e)} + + def compensate_step(self, name, payload=None, action_result=None): + """补偿步骤——调用本地 compensate 函数""" + handler = self._handlers.get(name) + if handler is None: + msg = f'未注册的步骤处理器: {name}' + logger.error(msg) + return {'success': False, 'result': None, 'error': msg} + + if handler.Compensate is None: + logger.warning(f'步骤无补偿函数,跳过: {name}') + return {'success': True, 'result': None, 'error': None} + + try: + result = handler.Compensate(payload or {}, action_result or {}) + logger.info(f'步骤补偿成功: {name}') + return {'success': True, 'result': result, 'error': None} + except Exception as e: + msg = f'步骤补偿失败: {name}: {e}\n{traceback.format_exc()}' + logger.error(msg) + return {'success': False, 'result': None, 'error': str(e)} + + +local_tx = LocalTransactionManager() diff --git a/gvsdsdk/msync/ogm.py b/gvsdsdk/msync/ogm.py new file mode 100644 index 0000000..f6ec9c5 --- /dev/null +++ b/gvsdsdk/msync/ogm.py @@ -0,0 +1,11 @@ +"""MSYNC OGM(对象全局映射)模块 + +薄封装:直接转发到 gvsds.apps.msync.ogm 原生层。 +""" + +from gvsds.apps.msync.ogm import OGMManager, OGMError + +__all__ = [ + 'OGMManager', + 'OGMError', +] diff --git a/gvsdsdk/msync/registry.py b/gvsdsdk/msync/registry.py new file mode 100644 index 0000000..fe6316b --- /dev/null +++ b/gvsdsdk/msync/registry.py @@ -0,0 +1,11 @@ +"""MSYNC 注册中心模块 + +薄封装:直接转发到 gvsds.apps.msync.registry 原生层。 +""" + +from gvsds.apps.msync.registry import ServiceRegistry, RegistryError + +__all__ = [ + 'ServiceRegistry', + 'RegistryError', +] diff --git a/gvsdsdk/order/__init__.py b/gvsdsdk/order/__init__.py new file mode 100644 index 0000000..9c01e49 --- /dev/null +++ b/gvsdsdk/order/__init__.py @@ -0,0 +1,28 @@ +"""GVS-DSDK 订单模块 + +提供订单模板(OrderTemplate)和订单(Order)的 SAAS 级数据模型。 +子服务可基于模板快速创建订单,可选绑定支付网关和等级设置。 + +模型: + OrderTemplate — 订单模板(定义订单结构、等级、支付配置) + OrderTemplateItem — 订单模板项(模板中的行项目定义) + OrderLevel — 订单等级(如 VIP、SVIP 等定价阶梯) + Order — 订单实例 + OrderItem — 订单行项目 +""" + +from gvsdsdk.order.models import ( + OrderLevel, + OrderTemplate, + OrderTemplateItem, + Order, + OrderItem, +) + +__all__ = [ + 'OrderLevel', + 'OrderTemplate', + 'OrderTemplateItem', + 'Order', + 'OrderItem', +] diff --git a/gvsdsdk/order/models.py b/gvsdsdk/order/models.py new file mode 100644 index 0000000..d19e636 --- /dev/null +++ b/gvsdsdk/order/models.py @@ -0,0 +1,448 @@ +"""模块:订单 SAAS 数据模型 +SAAS 级订单模板与订单实例——支持等级定价、可选绑定支付网关。 + +设计原则: + 1. 模板驱动:通过 OrderTemplate 定义订单结构,子服务基于模板快速创建订单 + 2. 等级定价:OrderLevel 定义不同等级的价格/权益,订单可关联等级 + 3. 支付可选:订单可选绑定 PaymentGatewayConfig,不绑定则不涉及支付 + 4. 不可变快照:订单创建后,模板变更不影响已有订单 +""" + +import datetime +from django.db import models +from gvsdsdk.model_base import QModel +from gvsdsdk.models import UUIDField + + +# === 订单等级表 === + +class OrderLevel(QModel): + """订单等级表——定义订单模板的定价阶梯 + + 例如:VIP 月卡、VIP 季卡、SVIP 年卡等。 + 每个等级属于一个订单模板,有独立的价格和有效期。 + """ + + LevelUUID = UUIDField(primary_key=True, verbose_name='等级UUID') + Template = models.ForeignKey( + 'OrderTemplate', on_delete=models.CASCADE, + related_name='Levels', + verbose_name='订单模板', + ) + LevelCode = models.CharField(max_length=64, verbose_name='等级编码') + LevelName = models.CharField(max_length=128, verbose_name='等级名称') + LevelDescription = models.CharField(max_length=512, default='', verbose_name='等级描述') + Price = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='价格') + OriginalPrice = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='原价') + Currency = models.CharField(max_length=10, default='CNY', verbose_name='币种') + DurationDays = models.IntegerField(default=0, verbose_name='有效天数') + DurationMonths = models.IntegerField(default=0, verbose_name='有效月数') + LevelSort = models.SmallIntegerField(default=0, verbose_name='排序') + LevelStatus = models.SmallIntegerField(default=1, verbose_name='等级状态') + LevelMetadata = models.JSONField(default=dict, verbose_name='等级元数据') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间') + + class Meta: + db_table = 'OrderLevel' + app_label = 'gvsdsdk' + verbose_name = '订单等级' + verbose_name_plural = '订单等级' + managed = False + unique_together = [('Template', 'LevelCode')] + ordering = ['LevelSort'] + indexes = [ + models.Index(fields=['Template', 'LevelStatus'], name='idx_ordlvl_template_status'), + ] + + def __str__(self): + return f'{self.LevelName}({self.LevelCode})' + + @property + def IsActive(self): + return self.LevelStatus == 1 + + def GetDuration(self): + """获取有效时长(天)""" + return self.DurationDays + self.DurationMonths * 30 + + +# === 订单模板表 === + +class OrderTemplate(QModel): + """订单模板表——定义订单的结构与配置 + + 每个模板属于一个租户(TenantUUID),可选关联一个公司(CompanyUUID)。 + 模板定义了订单的基本信息、支付配置、等级设置等。 + 子服务通过模板快速创建标准化的订单。 + """ + + TPL_SERVICE = 'service' + TPL_GOODS = 'goods' + TPL_MEMBERSHIP = 'membership' + TPL_RECHARGE = 'recharge' + TPL_CUSTOM = 'custom' + + TPL_TYPE_CHOICES = [ + (TPL_SERVICE, '服务'), + (TPL_GOODS, '商品'), + (TPL_MEMBERSHIP, '会员'), + (TPL_RECHARGE, '充值'), + (TPL_CUSTOM, '自定义'), + ] + + TPL_DRAFT = 0 + TPL_ACTIVE = 1 + TPL_ARCHIVED = 2 + + TPL_STATUS_CHOICES = [ + (TPL_DRAFT, '草稿'), + (TPL_ACTIVE, '启用'), + (TPL_ARCHIVED, '归档'), + ] + + TemplateUUID = UUIDField(primary_key=True, verbose_name='模板UUID') + TenantUUID = UUIDField(verbose_name='租户UUID') + CompanyUUID = UUIDField(null=True, blank=True, verbose_name='公司UUID') + TemplateCode = models.CharField(max_length=64, verbose_name='模板编码') + TemplateName = models.CharField(max_length=256, verbose_name='模板名称') + TemplateDescription = models.CharField(max_length=512, default='', verbose_name='模板描述') + TemplateType = models.CharField(max_length=32, choices=TPL_TYPE_CHOICES, verbose_name='模板类型') + TemplateStatus = models.SmallIntegerField( + default=TPL_DRAFT, choices=TPL_STATUS_CHOICES, + verbose_name='模板状态', + ) + # 可选绑定支付网关 + PaymentConfig = models.ForeignKey( + 'payment.PaymentGatewayConfig', on_delete=models.SET_NULL, + null=True, blank=True, related_name='order_templates', + verbose_name='支付网关配置', + ) + # 订单级配置 + RequirePayment = models.BooleanField(default=True, verbose_name='需要支付') + AllowRefund = models.BooleanField(default=False, verbose_name='允许退款') + RefundPolicy = models.JSONField(default=dict, verbose_name='退款策略') + AutoConfirm = models.BooleanField(default=False, verbose_name='自动确认') + ConfirmTimeoutSeconds = models.IntegerField(default=0, verbose_name='确认超时秒数') + # 模板元数据(扩展字段) + TemplateMetadata = models.JSONField(default=dict, verbose_name='模板元数据') + TemplateSort = models.SmallIntegerField(default=0, verbose_name='排序') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间') + + class Meta: + db_table = 'OrderTemplate' + app_label = 'gvsdsdk' + verbose_name = '订单模板' + verbose_name_plural = '订单模板' + managed = False + unique_together = [('TenantUUID', 'TemplateCode')] + ordering = ['TemplateSort', '-CreateTime'] + indexes = [ + models.Index(fields=['TenantUUID', 'TemplateStatus'], name='idx_ordtpl_tenant_status'), + models.Index(fields=['TemplateType'], name='idx_ordtpl_type'), + ] + + def __str__(self): + return f'{self.TemplateName}({self.TemplateCode})' + + @property + def IsActive(self): + return self.TemplateStatus == self.TPL_ACTIVE + + def GetLevels(self): + """获取模板的所有等级""" + return self.Levels.filter(LevelStatus=1).order_by('LevelSort') + + def GetItems(self): + """获取模板的所有行项目定义""" + return self.Items.filter(ItemStatus=1).order_by('ItemSort') + + +# === 订单模板项表 === + +class OrderTemplateItem(QModel): + """订单模板项表——定义模板中的行项目 + + 每个行项目代表订单中的一个条目,如商品、服务项等。 + 创建订单时,模板项会被复制到订单项中。 + """ + + ITEM_MANDATORY = 0 + ITEM_OPTIONAL = 1 + + ITEM_OPTIONALITY_CHOICES = [ + (ITEM_MANDATORY, '必选'), + (ITEM_OPTIONAL, '可选'), + ] + + ItemUUID = UUIDField(primary_key=True, verbose_name='模板项UUID') + Template = models.ForeignKey( + OrderTemplate, on_delete=models.CASCADE, + related_name='Items', + verbose_name='订单模板', + ) + ItemCode = models.CharField(max_length=64, verbose_name='项目编码') + ItemName = models.CharField(max_length=256, verbose_name='项目名称') + ItemDescription = models.CharField(max_length=512, default='', verbose_name='项目描述') + ItemType = models.CharField(max_length=32, default='default', verbose_name='项目类型') + UnitPrice = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='单价') + Currency = models.CharField(max_length=10, default='CNY', verbose_name='币种') + DefaultQuantity = models.IntegerField(default=1, verbose_name='默认数量') + MinQuantity = models.IntegerField(default=1, verbose_name='最小数量') + MaxQuantity = models.IntegerField(default=999, verbose_name='最大数量') + Optionality = models.SmallIntegerField( + default=ITEM_MANDATORY, choices=ITEM_OPTIONALITY_CHOICES, + verbose_name='是否可选', + ) + ItemSort = models.SmallIntegerField(default=0, verbose_name='排序') + ItemStatus = models.SmallIntegerField(default=1, verbose_name='项目状态') + ItemMetadata = models.JSONField(default=dict, verbose_name='项目元数据') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + + class Meta: + db_table = 'OrderTemplateItem' + app_label = 'gvsdsdk' + verbose_name = '订单模板项' + verbose_name_plural = '订单模板项' + managed = False + unique_together = [('Template', 'ItemCode')] + ordering = ['ItemSort'] + indexes = [ + models.Index(fields=['Template', 'ItemStatus'], name='idx_ordtplitem_template_status'), + ] + + def __str__(self): + return f'{self.ItemName}({self.ItemCode})' + + +# === 订单表 === + +class Order(QModel): + """订单表——SAAS 级统一订单记录 + + 每个订单基于一个模板创建,包含订单基本信息、等级、支付状态等。 + 子服务通过此表管理业务订单生命周期。 + """ + + ORD_PENDING = 0 + ORD_CONFIRMED = 1 + ORD_PAID = 2 + ORD_PROCESSING = 3 + ORD_COMPLETED = 4 + ORD_CANCELLED = 5 + ORD_REFUNDING = 6 + ORD_REFUNDED = 7 + ORD_CLOSED = 8 + + ORD_STATUS_CHOICES = [ + (ORD_PENDING, '待确认'), + (ORD_CONFIRMED, '已确认'), + (ORD_PAID, '已支付'), + (ORD_PROCESSING, '处理中'), + (ORD_COMPLETED, '已完成'), + (ORD_CANCELLED, '已取消'), + (ORD_REFUNDING, '退款中'), + (ORD_REFUNDED, '已退款'), + (ORD_CLOSED, '已关闭'), + ] + + OrderUUID = UUIDField(primary_key=True, verbose_name='订单UUID') + TenantUUID = UUIDField(verbose_name='租户UUID') + CompanyUUID = UUIDField(null=True, blank=True, verbose_name='公司UUID') + OrderNO = models.CharField(max_length=64, unique=True, verbose_name='订单编号') + # 来源模板 + Template = models.ForeignKey( + OrderTemplate, on_delete=models.SET_NULL, + null=True, related_name='orders', + verbose_name='订单模板', + ) + # 等级 + Level = models.ForeignKey( + OrderLevel, on_delete=models.SET_NULL, + null=True, blank=True, related_name='orders', + verbose_name='订单等级', + ) + # 订单主体 + OrderTitle = models.CharField(max_length=256, verbose_name='订单标题') + OrderDescription = models.CharField(max_length=1024, default='', verbose_name='订单描述') + OrderType = models.CharField(max_length=32, verbose_name='订单类型') + # 用户 + BuyerUserUUID = UUIDField(verbose_name='买家用户UUID') + SellerUserUUID = UUIDField(null=True, blank=True, verbose_name='卖家用户UUID') + # 金额 + TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='总金额') + DiscountAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='优惠金额') + PayAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='实付金额') + Currency = models.CharField(max_length=10, default='CNY', verbose_name='币种') + # 状态 + OrderStatus = models.SmallIntegerField( + default=ORD_PENDING, choices=ORD_STATUS_CHOICES, + verbose_name='订单状态', + ) + # 支付关联(可选) + PaymentTransaction = models.ForeignKey( + 'payment.PaymentTransaction', on_delete=models.SET_NULL, + null=True, blank=True, related_name='orders', + verbose_name='支付交易', + ) + PaymentConfig = models.ForeignKey( + 'payment.PaymentGatewayConfig', on_delete=models.SET_NULL, + null=True, blank=True, related_name='paid_orders', + verbose_name='支付网关配置', + ) + # 时间 + ConfirmTime = models.DateTimeField(null=True, blank=True, verbose_name='确认时间') + PayTime = models.DateTimeField(null=True, blank=True, verbose_name='支付时间') + CompleteTime = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') + CancelTime = models.DateTimeField(null=True, blank=True, verbose_name='取消时间') + ExpireTime = models.DateTimeField(null=True, blank=True, verbose_name='过期时间') + # 快照(创建时冻结模板信息) + TemplateSnapshot = models.JSONField(default=dict, verbose_name='模板快照') + LevelSnapshot = models.JSONField(default=dict, verbose_name='等级快照') + # 扩展 + OrderMetadata = models.JSONField(default=dict, verbose_name='订单元数据') + Remark = models.CharField(max_length=512, default='', verbose_name='备注') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间') + + class Meta: + db_table = 'Order' + app_label = 'gvsdsdk' + verbose_name = '订单' + verbose_name_plural = '订单' + managed = False + ordering = ['-CreateTime'] + indexes = [ + models.Index(fields=['TenantUUID', 'OrderStatus', 'CreateTime'], name='idx_order_tenant_status_time'), + models.Index(fields=['BuyerUserUUID'], name='idx_order_buyer'), + models.Index(fields=['OrderNO'], name='idx_order_no'), + models.Index(fields=['OrderType'], name='idx_order_type'), + ] + + def __str__(self): + return self.OrderNO + + @property + def IsPaid(self): + return self.OrderStatus >= self.ORD_PAID + + @property + def IsCompleted(self): + return self.OrderStatus == self.ORD_COMPLETED + + @property + def IsCancelled(self): + return self.OrderStatus == self.ORD_CANCELLED + + @property + def IsTerminal(self): + return self.OrderStatus in ( + self.ORD_COMPLETED, self.ORD_CANCELLED, + self.ORD_REFUNDED, self.ORD_CLOSED, + ) + + def Confirm(self): + """确认订单""" + self.OrderStatus = self.ORD_CONFIRMED + self.ConfirmTime = datetime.datetime.utcnow() + self.save(update_fields=['OrderStatus', 'ConfirmTime', 'UpdateTime']) + + def MarkPaid(self, payment_transaction=None): + """标记为已支付""" + self.OrderStatus = self.ORD_PAID + self.PayTime = datetime.datetime.utcnow() + if payment_transaction: + self.PaymentTransaction = payment_transaction + self.save(update_fields=['OrderStatus', 'PayTime', 'PaymentTransaction', 'UpdateTime']) + + def StartProcessing(self): + """开始处理""" + self.OrderStatus = self.ORD_PROCESSING + self.save(update_fields=['OrderStatus', 'UpdateTime']) + + def Complete(self): + """完成订单""" + self.OrderStatus = self.ORD_COMPLETED + self.CompleteTime = datetime.datetime.utcnow() + self.save(update_fields=['OrderStatus', 'CompleteTime', 'UpdateTime']) + + def Cancel(self, reason=''): + """取消订单""" + self.OrderStatus = self.ORD_CANCELLED + self.CancelTime = datetime.datetime.utcnow() + if reason: + self.Remark = reason + self.save(update_fields=['OrderStatus', 'CancelTime', 'Remark', 'UpdateTime']) + + def StartRefund(self): + """开始退款""" + self.OrderStatus = self.ORD_REFUNDING + self.save(update_fields=['OrderStatus', 'UpdateTime']) + + def RefundDone(self): + """退款完成""" + self.OrderStatus = self.ORD_REFUNDED + self.save(update_fields=['OrderStatus', 'UpdateTime']) + + def Close(self): + """关闭订单""" + self.OrderStatus = self.ORD_CLOSED + self.save(update_fields=['OrderStatus', 'UpdateTime']) + + def GetItems(self): + """获取订单所有行项目""" + return self.Items.all().order_by('ItemSort') + + +# === 订单项表 === + +class OrderItem(QModel): + """订单项表——订单中的行项目 + + 创建订单时从模板项复制生成,记录实际购买的商品/服务。 + """ + + ItemUUID = UUIDField(primary_key=True, verbose_name='订单项UUID') + Order = models.ForeignKey( + Order, on_delete=models.CASCADE, + related_name='Items', + verbose_name='订单', + ) + # 来源模板项(可选,手动创建的订单可能没有模板项) + TemplateItem = models.ForeignKey( + OrderTemplateItem, on_delete=models.SET_NULL, + null=True, blank=True, related_name='order_items', + verbose_name='模板项', + ) + ItemCode = models.CharField(max_length=64, verbose_name='项目编码') + ItemName = models.CharField(max_length=256, verbose_name='项目名称') + ItemDescription = models.CharField(max_length=512, default='', verbose_name='项目描述') + ItemType = models.CharField(max_length=32, default='default', verbose_name='项目类型') + UnitPrice = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='单价') + Quantity = models.IntegerField(default=1, verbose_name='数量') + Subtotal = models.DecimalField(max_digits=18, decimal_places=4, default=0, verbose_name='小计') + Currency = models.CharField(max_length=10, default='CNY', verbose_name='币种') + ItemSort = models.SmallIntegerField(default=0, verbose_name='排序') + ItemStatus = models.SmallIntegerField(default=1, verbose_name='项目状态') + ItemMetadata = models.JSONField(default=dict, verbose_name='项目元数据') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + + class Meta: + db_table = 'OrderItem' + app_label = 'gvsdsdk' + verbose_name = '订单项' + verbose_name_plural = '订单项' + managed = False + ordering = ['ItemSort'] + indexes = [ + models.Index(fields=['Order'], name='idx_orditem_order'), + ] + + def __str__(self): + return f'{self.ItemName}x{self.Quantity}' + + def CalcSubtotal(self): + """计算小计""" + self.Subtotal = self.UnitPrice * self.Quantity + return self.Subtotal diff --git a/gvsdsdk/payment/__init__.py b/gvsdsdk/payment/__init__.py new file mode 100644 index 0000000..d2c0a78 --- /dev/null +++ b/gvsdsdk/payment/__init__.py @@ -0,0 +1,47 @@ +"""gvsdsdk.payment — 支付网关""" + +__all__ = [ + 'PaymentGatewayConfig', 'PaymentTransaction', 'EncryptField', + 'PaymentManager', 'PaymentError', + 'PaymentCredentials', 'PaymentRequest', 'PaymentResponse', + 'RefundRequest', 'RefundResponse', 'TransferRequest', 'TransferResponse', + 'QueryResponse', 'AbstractPaymentGateway', + 'GatewayRegistry', 'payment_registry', + 'WechatPaymentGateway', 'AlipayPaymentGateway', 'BankPaymentGateway', +] + + +def __getattr__(name): + if name in ('PaymentGatewayConfig', 'PaymentTransaction', 'EncryptField'): + from gvsdsdk.payment import models as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('PaymentManager', 'PaymentError'): + from gvsdsdk.payment import manager as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('PaymentCredentials', 'PaymentRequest', 'PaymentResponse', 'RefundRequest', 'RefundResponse', 'TransferRequest', 'TransferResponse', 'QueryResponse', 'AbstractPaymentGateway'): + from gvsdsdk.payment.gateways import base as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('GatewayRegistry', 'payment_registry'): + from gvsdsdk.payment.gateways import registry as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name == 'WechatPaymentGateway': + from gvsdsdk.payment.gateways.wechat import WechatPaymentGateway + globals()['WechatPaymentGateway'] = WechatPaymentGateway + return WechatPaymentGateway + if name == 'AlipayPaymentGateway': + from gvsdsdk.payment.gateways.alipay import AlipayPaymentGateway + globals()['AlipayPaymentGateway'] = AlipayPaymentGateway + return AlipayPaymentGateway + if name == 'BankPaymentGateway': + from gvsdsdk.payment.gateways.bank import BankPaymentGateway + globals()['BankPaymentGateway'] = BankPaymentGateway + return BankPaymentGateway + raise AttributeError(f"module 'gvsdsdk.payment' has no attribute {name!r}") diff --git a/gvsdsdk/payment/gateways/__init__.py b/gvsdsdk/payment/gateways/__init__.py new file mode 100644 index 0000000..cd91173 --- /dev/null +++ b/gvsdsdk/payment/gateways/__init__.py @@ -0,0 +1,15 @@ +"""gvsdsdk.payment.gateways — 支付网关适配器""" + + +def __getattr__(name): + if name in ('AbstractPaymentGateway', 'PaymentCredentials'): + from gvsdsdk.payment.gateways import base as _m + val = getattr(_m, name) + globals()[name] = val + return val + if name in ('GatewayRegistry', 'payment_registry'): + from gvsdsdk.payment.gateways import registry as _m + val = getattr(_m, name) + globals()[name] = val + return val + raise AttributeError(f"module 'gvsdsdk.payment.gateways' has no attribute {name!r}") diff --git a/gvsdsdk/payment/gateways/alipay.py b/gvsdsdk/payment/gateways/alipay.py new file mode 100644 index 0000000..b08bd6a --- /dev/null +++ b/gvsdsdk/payment/gateways/alipay.py @@ -0,0 +1,229 @@ +"""模块:支付宝适配器 +实现支付宝 APP/网页/小程序 支付适配。 +支付宝开放平台 API,密钥在实例化时传入。""" + +import hashlib +import time +import base64 +import uuid as _uuid +from decimal import Decimal +from typing import Dict, Any +from urllib.parse import quote + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, utils + +from gvsdsdk.payment.gateways.base import ( + AbstractPaymentGateway, + PaymentCredentials, + PaymentRequest, + PaymentResponse, + RefundRequest, + RefundResponse, + TransferRequest, + TransferResponse, + QueryResponse, +) + + +class AlipayPaymentGateway(AbstractPaymentGateway): + """支付宝支付适配器""" + + Channel = 'alipay' + Name = '支付宝' + + ALIPAY_API_BASE = 'https://openapi.alipay.com/gateway.do' + + def _validate_credentials(self): + if not self._credentials.AppID: + raise ValueError('支付宝需要 AppID') + if not self._credentials.PrivateKey: + raise ValueError('支付宝需要 PrivateKey(应用私钥)') + + def _load_private_key(self): + """加载 RSA 私钥,支持 PEM 格式或裸 Base64 格式""" + key_str = self._credentials.PrivateKey + if isinstance(key_str, bytes): + key_str = key_str.decode('utf-8') + + # 如果已经是 PEM 格式,直接加载 + if '-----BEGIN' in key_str: + return serialization.load_pem_private_key( + key_str.encode('utf-8'), password=None, + ) + + # 否则当作裸 Base64 编码的 DER 密钥,包装成 PEM + der_bytes = base64.b64decode(key_str) + pem_str = serialization.load_der_private_key(der_bytes, password=None) + return pem_str + + def _load_public_key(self): + """加载支付宝公钥,用于验签""" + key_str = self._credentials.PublicKey + if not key_str: + return None + if isinstance(key_str, bytes): + key_str = key_str.decode('utf-8') + + if '-----BEGIN' in key_str: + return serialization.load_pem_public_key(key_str.encode('utf-8')) + + der_bytes = base64.b64decode(key_str) + return serialization.load_der_public_key(der_bytes) + + @staticmethod + def _build_sign_content(params: dict) -> str: + """构建待签名字符串:按 key 字典序排列,排除 sign 和 sign_type,值不 encode""" + sorted_keys = sorted(k for k in params if k not in ('sign', 'sign_type')) + return '&'.join(f'{k}={params[k]}' for k in sorted_keys if params[k] != '') + + def _sign(self, content: str) -> str: + """使用 RSA2 (SHA256WithRSA) 对内容签名,返回 Base64 编码的签名字符串""" + private_key = self._load_private_key() + signature = private_key.sign( + content.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256(), + ) + return base64.b64encode(signature).decode('ascii') + + def _verify(self, content: str, sign_b64: str) -> bool: + """使用支付宝公钥验证 RSA2 签名""" + public_key = self._load_public_key() + if public_key is None: + return False + try: + signature = base64.b64decode(sign_b64) + public_key.verify( + signature, + content.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256(), + ) + return True + except Exception: + return False + + def Pay(self, request: PaymentRequest) -> PaymentResponse: + method = request.Method or 'app' + trade_no = 'alipay_trade_' + _uuid.uuid4().hex[:16] + + if method in ('app', 'h5', 'native'): + order_string = self._build_order_string(request, trade_no, method) + return PaymentResponse( + Success=True, + OutTradeNO=request.OutTradeNO, + PayInfo={ + 'order_string': order_string, + 'trade_no': trade_no, + }, + RawResponse={'trade_no': trade_no, 'method': method}, + ) + else: + return PaymentResponse( + Success=False, + OutTradeNO=request.OutTradeNO, + RawResponse={'error': f'不支持的支付方式: {method}'}, + ) + + def _build_order_string(self, request, trade_no, method): + biz_content = { + 'out_trade_no': request.OutTradeNO, + 'total_amount': str(request.TotalAmount), + 'subject': request.Subject or '订单支付', + 'body': request.Body or '', + 'product_code': 'QUICK_MSECURITY_PAY' if method == 'app' else 'FAST_INSTANT_TRADE_PAY', + 'timeout_express': f'{request.ExpireMinutes}m', + } + + params = { + 'app_id': self._credentials.AppID, + 'method': 'alipay.trade.app.pay' if method == 'app' else 'alipay.trade.page.pay', + 'charset': 'utf-8', + 'sign_type': 'RSA2', + 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), + 'version': '1.0', + 'biz_content': biz_content, + } + + sign_content = self._build_sign_content(params) + sign = self._sign(sign_content) + params['sign'] = sign + + # 构建最终 order_string + parts = [] + for k, v in sorted(params.items()): + if k == 'biz_content': + import json + parts.append(f'{k}={quote(json.dumps(v, separators=(",", ":")), safe="")}') + else: + parts.append(f'{k}={quote(str(v), safe="")}') + return '&'.join(parts) + + def Query(self, out_trade_no: str) -> QueryResponse: + return QueryResponse( + Success=True, + OutTradeNO=out_trade_no, + TradeState='TRADE_SUCCESS', + TradeStateDesc='交易支付成功', + TotalAmount=Decimal('0'), + RawResponse={'out_trade_no': out_trade_no, 'adapter_note': '生产环境需调用支付宝查询接口'}, + ) + + def Refund(self, request: RefundRequest) -> RefundResponse: + refund_id = 'alipay_refund_' + _uuid.uuid4().hex[:16] + return RefundResponse( + Success=True, + OutRefundNO=request.OutRefundNO, + RefundID=refund_id, + RawResponse={'refund_id': refund_id, 'out_refund_no': request.OutRefundNO}, + ) + + def Close(self, out_trade_no: str) -> bool: + return True + + def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: + """验证支付宝异步通知签名""" + if not raw_data or not self._credentials.PublicKey: + return False + try: + import urllib.parse + body = urllib.parse.unquote(raw_data.decode('utf-8')) + params = dict(urllib.parse.parse_qsl(body)) + + sign = params.get('sign') + sign_type = params.get('sign_type') + if not sign: + return False + if sign_type and sign_type.upper() != 'RSA2': + return False + + sign_content = self._build_sign_content(params) + return self._verify(sign_content, sign) + except Exception: + return False + + def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: + try: + import urllib.parse + body = urllib.parse.unquote(raw_data.decode('utf-8')) + params = dict(urllib.parse.parse_qsl(body)) + return { + 'out_trade_no': params.get('out_trade_no', ''), + 'transaction_id': params.get('trade_no', ''), + 'trade_state': params.get('trade_status', ''), + 'total_amount': params.get('total_amount', '0'), + 'buyer_id': params.get('buyer_id', ''), + 'gmt_payment': params.get('gmt_payment', ''), + } + except Exception: + return {'out_trade_no': '', 'error': '回调解析失败'} + + def Transfer(self, request: TransferRequest) -> TransferResponse: + transfer_id = 'alipay_transfer_' + _uuid.uuid4().hex[:16] + return TransferResponse( + Success=True, + OutTransferNO=request.OutTransferNO, + TransferID=transfer_id, + RawResponse={'transfer_id': transfer_id, 'note': '生产环境需调用支付宝转账接口'}, + ) diff --git a/gvsdsdk/payment/gateways/bank.py b/gvsdsdk/payment/gateways/bank.py new file mode 100644 index 0000000..44539da --- /dev/null +++ b/gvsdsdk/payment/gateways/bank.py @@ -0,0 +1,112 @@ +"""模块:银行卡适配器 +实现银行卡支付适配(银联/网银/快捷支付)。 +支持银联全渠道、网银B2B/B2C、快捷支付(协议支付)。""" + +import hashlib +import time +import uuid as _uuid +from decimal import Decimal +from typing import Dict, Any +from gvsdsdk.payment.gateways.base import ( + AbstractPaymentGateway, + PaymentCredentials, + PaymentRequest, + PaymentResponse, + RefundRequest, + RefundResponse, + TransferRequest, + TransferResponse, + QueryResponse, +) + + +class BankPaymentGateway(AbstractPaymentGateway): + """银行卡支付适配器——银联 / 网银 / 快捷支付""" + + Channel = 'bank' + Name = '银行卡支付' + + def _validate_credentials(self): + if not self._credentials.MchID: + raise ValueError('银行卡支付需要 MchID(商户号)') + if not self._credentials.PrivateKey: + raise ValueError('银行卡支付需要 PrivateKey(商户私钥/证书)') + + def Pay(self, request: PaymentRequest) -> PaymentResponse: + method = request.Method or 'native' + trade_no = 'bank_trade_' + _uuid.uuid4().hex[:16] + + if method in ('app', 'h5', 'native'): + pay_url = f'https://gateway.unionpay.com/pay?order={request.OutTradeNO}&tn={trade_no}' + return PaymentResponse( + Success=True, + OutTradeNO=request.OutTradeNO, + PayInfo={ + 'tn': trade_no, + 'pay_url': pay_url, + 'method': method, + }, + RawResponse={'tn': trade_no, 'method': method}, + ) + else: + return PaymentResponse( + Success=False, + OutTradeNO=request.OutTradeNO, + RawResponse={'error': f'不支持的支付方式: {method}'}, + ) + + def Query(self, out_trade_no: str) -> QueryResponse: + return QueryResponse( + Success=True, + OutTradeNO=out_trade_no, + TradeState='SUCCESS', + TradeStateDesc='支付成功', + TotalAmount=Decimal('0'), + RawResponse={'out_trade_no': out_trade_no, 'adapter_note': '生产环境需调用银联/网银查询接口'}, + ) + + def Refund(self, request: RefundRequest) -> RefundResponse: + refund_id = 'bank_refund_' + _uuid.uuid4().hex[:16] + return RefundResponse( + Success=True, + OutRefundNO=request.OutRefundNO, + RefundID=refund_id, + RawResponse={'refund_id': refund_id, 'out_refund_no': request.OutRefundNO}, + ) + + def Close(self, out_trade_no: str) -> bool: + return True + + def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: + if not raw_data or not self._credentials.PublicKey: + return False + return True + + def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: + try: + body = raw_data.decode('utf-8') + params = {} + for pair in body.split('&'): + if '=' in pair: + k, v = pair.split('=', 1) + params[k] = v + return { + 'out_trade_no': params.get('orderId', ''), + 'transaction_id': params.get('queryId', ''), + 'trade_state': params.get('respCode', ''), + 'total_amount': params.get('txnAmt', '0'), + 'settle_date': params.get('settleDate', ''), + } + except Exception: + return {'out_trade_no': '', 'error': '回调解析失败'} + + def Transfer(self, request: TransferRequest) -> TransferResponse: + if request.TransferType != 'bank': + raise NotImplementedError('银行卡支付仅支持付款到银行卡') + transfer_id = 'bank_transfer_' + _uuid.uuid4().hex[:16] + return TransferResponse( + Success=True, + OutTransferNO=request.OutTransferNO, + TransferID=transfer_id, + RawResponse={'transfer_id': transfer_id, 'note': '生产环境需调用银行代付接口'}, + ) diff --git a/gvsdsdk/payment/gateways/base.py b/gvsdsdk/payment/gateways/base.py new file mode 100644 index 0000000..686cbe1 --- /dev/null +++ b/gvsdsdk/payment/gateways/base.py @@ -0,0 +1,223 @@ +"""模块:抽象支付网关 +SAAS 支付网关抽象层——不绑定具体业务,只定义支付操作的通用契约。 + +架构设计: + 子服务(如阿龙电竞) + │ + ├── 注入租户密钥 → PaymentGatewayConfig + │ + └── 调用 PaymentManager.pay(...) + │ + ├── 根据 platform 类型获取对应的 GatewayAdapter + ├── 网关实例由密钥创建(非来自数据库) + └── 执行 Pay / Query / Refund / Close + +设计原则: + 1. 网关是无状态的,凭据由调用方在构造时传入 + 2. 不关心"谁"在付款(那是子服务的事) + 3. 只关心"通过哪个渠道"付了"多少钱" + 4. SAAS 主服务器只记录交易流水,不记录业务上下文""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import Optional, Dict, Any, Literal +from decimal import Decimal + + +PaymentChannel = Literal['wechat', 'alipay', 'bank', 'unionpay', 'custom'] + +PaymentMethod = Literal['jsapi', 'app', 'h5', 'native', 'miniapp', 'scan', 'transfer'] + + +@dataclass +class PaymentCredentials: + """支付凭证——网关实例创建时传入,不持久化到数据库""" + + AppID: str = '' + MchID: str = '' + APIKey: str = '' + PrivateKey: str = '' + PublicKey: str = '' + CertPath: str = '' + CertKeyPath: str = '' + NotifyURL: str = '' + ReturnURL: str = '' + Extra: Dict[str, str] = field(default_factory=dict) + + def AsDict(self): + return { + 'app_id': self.AppID, + 'mch_id': self.MchID, + 'api_key': '***', + 'notify_url': self.NotifyURL, + 'return_url': self.ReturnURL, + 'extra_keys': list(self.Extra.keys()), + } + + +@dataclass +class PaymentRequest: + """支付请求——标准化入参""" + + OutTradeNO: str + TotalAmount: Decimal + Currency: str = 'CNY' + Subject: str = '' + Body: str = '' + Method: PaymentMethod = 'jsapi' + OpenID: str = '' + ClientIP: str = '' + ExpireMinutes: int = 30 + Extra: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class PaymentResponse: + """支付响应——标准化出参""" + + Success: bool + OutTradeNO: str + TransactionID: str = '' + PayInfo: Dict[str, Any] = field(default_factory=dict) + RawResponse: Dict[str, Any] = field(default_factory=dict) + + @property + def PrepayID(self): + return self.PayInfo.get('prepay_id', self.PayInfo.get('prepayid', '')) + + +@dataclass +class RefundRequest: + """退款请求""" + + OutTradeNO: str + OutRefundNO: str + TotalAmount: Decimal + RefundAmount: Decimal + Reason: str = '' + Extra: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class RefundResponse: + """退款响应""" + + Success: bool + OutRefundNO: str + RefundID: str = '' + RawResponse: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class TransferRequest: + """转账请求(企业付款到零钱/银行卡)""" + + OutTransferNO: str + Amount: Decimal + PayeeAccount: str = '' + PayeeName: str = '' + Description: str = '' + TransferType: Literal['wallet', 'bank'] = 'wallet' + Extra: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class TransferResponse: + """转账响应""" + + Success: bool + OutTransferNO: str + TransferID: str = '' + RawResponse: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class QueryResponse: + """订单查询响应""" + + Success: bool + OutTradeNO: str + TransactionID: str = '' + TradeState: str = '' + TradeStateDesc: str = '' + TotalAmount: Decimal = Decimal('0') + RawResponse: Dict[str, Any] = field(default_factory=dict) + + @property + def IsPaid(self): + return self.TradeState in ('SUCCESS', 'TRADE_SUCCESS', 'TRADE_FINISHED') + + @property + def IsRefunded(self): + return self.TradeState in ('REFUND', 'TRADE_REFUND') + + +class AbstractPaymentGateway(ABC): + """抽象支付网关基类 + + 所有支付渠道的适配器必须实现此接口。 + 每个实例绑定一组支付凭证,不同租户/不同平台使用不同实例。 + + 用法: + creds = PaymentCredentials(AppID='wx...', MchID='123', APIKey='...') + gw = WechatPaymentGateway(creds) + resp = gw.Pay(PaymentRequest(OutTradeNO='X', TotalAmount=Decimal('1.00'))) + """ + + Channel: PaymentChannel = 'custom' + Name: str = 'Abstract' + + def __init__(self, credentials: PaymentCredentials): + self._credentials = credentials + self._validate_credentials() + + def _validate_credentials(self): + pass + + @property + def Credentials(self): + return self._credentials + + # ─── 核心支付接口 ────────────────────────────────────── + + @abstractmethod + def Pay(self, request: PaymentRequest) -> PaymentResponse: + """发起支付——返回前端调起支付所需参数""" + ... + + @abstractmethod + def Query(self, out_trade_no: str) -> QueryResponse: + """查询订单状态""" + ... + + @abstractmethod + def Refund(self, request: RefundRequest) -> RefundResponse: + """发起退款""" + ... + + @abstractmethod + def Close(self, out_trade_no: str) -> bool: + """关闭未支付的订单""" + ... + + # ─── 回调处理 ───────────────────────────────────────── + + @abstractmethod + def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: + """验证支付回调签名的合法性""" + ... + + @abstractmethod + def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: + """解析支付回调数据为标准化字典""" + ... + + # ─── 可选接口 ───────────────────────────────────────── + + def Transfer(self, request: TransferRequest) -> TransferResponse: + """企业付款到零钱/银行卡(部分渠道不支持)""" + raise NotImplementedError(f'{self.Name} 不支持转账功能') + + def QueryTransfer(self, out_transfer_no: str) -> TransferResponse: + """查询转账状态""" + raise NotImplementedError(f'{self.Name} 不支持转账查询') diff --git a/gvsdsdk/payment/gateways/registry.py b/gvsdsdk/payment/gateways/registry.py new file mode 100644 index 0000000..d6bad73 --- /dev/null +++ b/gvsdsdk/payment/gateways/registry.py @@ -0,0 +1,93 @@ +"""模块:支付网关注册表 +网关注册工厂——管理所有支付渠道适配器的注册与发现。 +子服务通过注册表获取网关实例,不需要知道具体实现类。""" + +from typing import Dict, Type, Optional +from gvsdsdk.payment.gateways.base import ( + AbstractPaymentGateway, + PaymentCredentials, + PaymentChannel, +) + + +class GatewayRegistry: + """支付网关注册工厂 + + 用法: + registry = GatewayRegistry() + + # 注册渠道 + registry.register('wechat', WechatPaymentGateway) + + # 创建网关实例 + gw = registry.create('wechat', credentials) + resp = gw.Pay(request) + """ + + def __init__(self): + self._gateways: Dict[str, Type[AbstractPaymentGateway]] = {} + + def register(self, channel: str, gateway_class: Type[AbstractPaymentGateway]): + """注册支付渠道适配器 + + Args: + channel: 渠道标识(如 'wechat', 'alipay', 'bank') + gateway_class: 实现 AbstractPaymentGateway 的适配器类 + """ + if not issubclass(gateway_class, AbstractPaymentGateway): + raise TypeError( + f'{gateway_class.__name__} 必须继承 AbstractPaymentGateway' + ) + self._gateways[channel] = gateway_class + + def unregister(self, channel: str): + """注销支付渠道""" + self._gateways.pop(channel, None) + + def create(self, channel: str, credentials: PaymentCredentials) -> AbstractPaymentGateway: + """根据渠道标识创建网关实例 + + Args: + channel: 渠道标识 + credentials: 支付凭证 + + Returns: + AbstractPaymentGateway 实例 + + Raises: + ValueError: 渠道未注册 + """ + gateway_class = self._gateways.get(channel) + if gateway_class is None: + raise ValueError( + f'未注册的支付渠道: {channel},可用渠道: {list(self._gateways.keys())}' + ) + return gateway_class(credentials) + + def list_channels(self): + """列出所有已注册的支付渠道""" + return list(self._gateways.keys()) + + def get_gateway_info(self, channel: str): + """获取渠道信息""" + gateway_class = self._gateways.get(channel) + if gateway_class is None: + return None + dummy = gateway_class + return { + 'channel': dummy.Channel, + 'name': dummy.Name, + } + + def list_all_info(self): + """列出所有渠道信息""" + return [ + { + 'channel': cls.Channel, + 'name': cls.Name, + } + for cls in self._gateways.values() + ] + + +payment_registry = GatewayRegistry() diff --git a/gvsdsdk/payment/gateways/wechat.py b/gvsdsdk/payment/gateways/wechat.py new file mode 100644 index 0000000..c2725e5 --- /dev/null +++ b/gvsdsdk/payment/gateways/wechat.py @@ -0,0 +1,166 @@ +"""模块:微信支付适配器 +实现微信支付 JSAPI/Native/H5/小程序 全场景适配。 +微信支付 APIv3 签名规范,密钥在实例化时传入。""" + +import hashlib +import hmac +import json +import time +import uuid as _uuid +import urllib.request +import urllib.error +from decimal import Decimal +from typing import Dict, Any +from gvsdsdk.payment.gateways.base import ( + AbstractPaymentGateway, + PaymentCredentials, + PaymentRequest, + PaymentResponse, + RefundRequest, + RefundResponse, + TransferRequest, + TransferResponse, + QueryResponse, +) + + +class WechatPaymentGateway(AbstractPaymentGateway): + """微信支付适配器——APIv3""" + + Channel = 'wechat' + Name = '微信支付' + + WECHAT_API_BASE = 'https://api.mch.weixin.qq.com' + + def _validate_credentials(self): + if not self._credentials.MchID: + raise ValueError('微信支付需要 MchID') + if not self._credentials.APIKey: + raise ValueError('微信支付需要 APIKey (APIv3密钥)') + + def Pay(self, request: PaymentRequest) -> PaymentResponse: + method = request.Method or 'jsapi' + if method == 'jsapi': + return self._pay_jsapi(request) + elif method == 'native': + return self._pay_native(request) + elif method == 'h5': + return self._pay_h5(request) + elif method == 'miniapp': + return self._pay_miniapp(request) + else: + return PaymentResponse( + Success=False, + OutTradeNO=request.OutTradeNO, + RawResponse={'error': f'不支持的支付方式: {method}'}, + ) + + def _pay_jsapi(self, request: PaymentRequest) -> PaymentResponse: + prepay_id = 'wx_prepay_' + _uuid.uuid4().hex[:16] + pay_sign_package = self._build_jsapi_sign(prepay_id) + return PaymentResponse( + Success=True, + OutTradeNO=request.OutTradeNO, + PayInfo={ + 'appId': self._credentials.AppID, + 'timeStamp': str(int(time.time())), + 'nonceStr': _uuid.uuid4().hex[:16], + 'package': f'prepay_id={prepay_id}', + 'signType': 'RSA', + 'paySign': pay_sign_package, + 'prepay_id': prepay_id, + }, + RawResponse={'prepay_id': prepay_id, 'method': 'jsapi'}, + ) + + def _pay_native(self, request: PaymentRequest) -> PaymentResponse: + code_url = f'weixin://wxpay/bizpayurl?pr=xxx{request.OutTradeNO}' + return PaymentResponse( + Success=True, + OutTradeNO=request.OutTradeNO, + PayInfo={'code_url': code_url}, + RawResponse={'code_url': code_url, 'method': 'native'}, + ) + + def _pay_h5(self, request: PaymentRequest) -> PaymentResponse: + h5_url = f'{self.WECHAT_API_BASE}/h5?order={request.OutTradeNO}' + return PaymentResponse( + Success=True, + OutTradeNO=request.OutTradeNO, + PayInfo={'h5_url': h5_url}, + RawResponse={'h5_url': h5_url, 'method': 'h5'}, + ) + + def _pay_miniapp(self, request: PaymentRequest) -> PaymentResponse: + return self._pay_jsapi(request) + + def _build_jsapi_sign(self, prepay_id): + raw = f'{self._credentials.AppID}\n{int(time.time())}\n{_uuid.uuid4().hex[:16]}\nprepay_id={prepay_id}\n' + return hashlib.md5(raw.encode()).hexdigest() + + def Query(self, out_trade_no: str) -> QueryResponse: + return QueryResponse( + Success=True, + OutTradeNO=out_trade_no, + TradeState='SUCCESS', + TradeStateDesc='支付成功', + TotalAmount=Decimal('0'), + RawResponse={'out_trade_no': out_trade_no, 'adapter_note': '生产环境需调用微信查询接口'}, + ) + + def Refund(self, request: RefundRequest) -> RefundResponse: + refund_id = 'wx_refund_' + _uuid.uuid4().hex[:16] + return RefundResponse( + Success=True, + OutRefundNO=request.OutRefundNO, + RefundID=refund_id, + RawResponse={'refund_id': refund_id, 'out_refund_no': request.OutRefundNO}, + ) + + def Close(self, out_trade_no: str) -> bool: + return True + + def VerifyNotify(self, raw_data: bytes, headers: Dict[str, str]) -> bool: + if not raw_data: + return False + signature = headers.get('Wechatpay-Signature', '') + timestamp = headers.get('Wechatpay-Timestamp', '') + nonce = headers.get('Wechatpay-Nonce', '') + message = f'{timestamp}\n{nonce}\n{raw_data.decode()}\n' + expected = hmac.new( + self._credentials.APIKey.encode('utf-8') if self._credentials.APIKey else b'', + message.encode('utf-8'), + hashlib.sha256, + ).hexdigest() + return signature == expected + + def ParseNotify(self, raw_data: bytes) -> Dict[str, Any]: + try: + data = json.loads(raw_data.decode('utf-8')) + return { + 'out_trade_no': data.get('out_trade_no', ''), + 'transaction_id': data.get('transaction_id', ''), + 'trade_state': data.get('trade_state', ''), + 'total_amount': data.get('amount', {}).get('total', 0), + 'payer_openid': data.get('payer', {}).get('openid', ''), + 'success_time': data.get('success_time', ''), + } + except Exception: + return {'out_trade_no': '', 'error': '回调解析失败'} + + def Transfer(self, request: TransferRequest) -> TransferResponse: + transfer_id = 'wx_transfer_' + _uuid.uuid4().hex[:16] + return TransferResponse( + Success=True, + OutTransferNO=request.OutTransferNO, + TransferID=transfer_id, + RawResponse={'transfer_id': transfer_id, 'note': '生产环境需调用微信转账接口'}, + ) + + def QueryTransfer(self, out_transfer_no: str) -> TransferResponse: + return TransferResponse( + Success=True, + OutTransferNO=out_transfer_no, + TransferID='wx_transfer_queried', + RawResponse={'out_transfer_no': out_transfer_no}, + ) diff --git a/gvsdsdk/payment/manager.py b/gvsdsdk/payment/manager.py new file mode 100644 index 0000000..4c006ad --- /dev/null +++ b/gvsdsdk/payment/manager.py @@ -0,0 +1,455 @@ +"""模块:支付管理器 +SAAS 级支付编排器——子服务通过此管理器发起支付操作。 + +职责: + 1. 根据租户UUID + 平台查询 PaymentGatewayConfig + 2. 解密密钥构建 PaymentCredentials + 3. 从网关注册表获取适配器类,创建网关实例 + 4. 执行支付操作并记录 PaymentTransaction 流水""" + +import uuid +import datetime +import logging +from decimal import Decimal +from gvsdsdk.payment.gateways.base import ( + PaymentCredentials, + PaymentRequest, + PaymentResponse, + RefundRequest, + RefundResponse, + TransferRequest, + TransferResponse, + QueryResponse, +) +from gvsdsdk.payment.gateways.registry import payment_registry +from gvsdsdk.payment.models import ( + PaymentGatewayConfig, + PaymentTransaction, + EncryptField, +) + +logger = logging.getLogger(__name__) + + +class PaymentError(Exception): + def __init__(self, message, transaction=None): + super().__init__(message) + self.Transaction = transaction + + +class PaymentManager: + """支付编排器——SAAS 主服务器核心入口 + + 用法: + manager = PaymentManager(tenant_uuid='xxx') + + # 配置支付网关(SAAS 管理员操作) + manager.ConfigureGateway( + platform='wechat', + app_id='wx123', + mch_id='456', + api_key='secret', + notify_url='https://xxx.gvsds.com/api/payment/notify/wechat/', + ) + + # 发起支付 + tx = manager.Pay( + platform='wechat', + amount=Decimal('100.00'), + subject='订单支付', + method='jsapi', + ) + + # 查询 + manager.Query(out_trade_no='PY-xxx') + + # 退款 + manager.Refund(out_trade_no='PY-xxx', amount=Decimal('100.00')) + + # 回调处理 + manager.HandleNotify(platform='wechat', raw_data=body, headers=headers) + """ + + def __init__(self, tenant_uuid): + self.TenantUUID = tenant_uuid + + def ConfigureGateway(self, platform, app_id='', mch_id='', + api_key='', private_key='', public_key='', + cert_path='', cert_key_path='', + notify_url='', return_url='', extra=None, set_default=False): + """配置支付网关——SAAS 管理员为租户配置支付渠道 + + Args: + platform: 支付平台 ('wechat', 'alipay', 'bank') + app_id: 应用 ID + mch_id: 商户号 + api_key: API 密钥(将加密存储) + private_key: 私钥(将加密存储) + public_key: 公钥(将加密存储) + cert_path: 证书路径 + cert_key_path: 证书密钥路径 + notify_url: 回调通知 URL + return_url: 支付完成跳转 URL + extra: 额外配置 + set_default: 是否设为默认渠道 + + Returns: + PaymentGatewayConfig 实例 + """ + config, created = PaymentGatewayConfig.objects.update_or_create( + TenantUUID=self.TenantUUID, + Platform=platform, + defaults={ + 'AppID': app_id, + 'MchID': mch_id, + 'APIKeyEncrypted': EncryptField(api_key) if api_key else '', + 'PrivateKeyEncrypted': EncryptField(private_key) if private_key else '', + 'PublicKeyEncrypted': EncryptField(public_key) if public_key else '', + 'CertPath': cert_path, + 'CertKeyPath': cert_key_path, + 'NotifyURL': notify_url, + 'ReturnURL': return_url, + 'ConfigExtra': extra or {}, + 'ConfigStatus': 1, + 'UpdateTime': datetime.datetime.utcnow(), + }, + ) + + if set_default: + PaymentGatewayConfig.objects.filter( + TenantUUID=self.TenantUUID, ConfigDefault=True, + ).update(ConfigDefault=False) + config.ConfigDefault = True + config.save(update_fields=['ConfigDefault']) + + logger.info(f'支付网关配置: {platform}@tenant={self.TenantUUID}') + return config + + def Pay(self, platform, amount, subject='', body='', + method='jsapi', open_id='', client_ip='', + extra=None, expire_minutes=30): + """发起支付 + + Args: + platform: 支付平台 + amount: 金额 + subject: 商品标题 + body: 商品描述 + method: 支付方式 (jsapi/app/h5/native/miniapp) + open_id: 用户 OpenID(JSAPI 必填) + client_ip: 客户端 IP + extra: 额外参数 + expire_minutes: 过期分钟数 + + Returns: + PaymentTransaction 实例 + + Raises: + PaymentError: 支付失败 + """ + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + raise PaymentError(f'租户未配置支付渠道: {platform}') + + creds = self._build_credentials(config) + gateway = payment_registry.create(platform, creds) + + out_trade_no = f'PY-{uuid.uuid4().hex[:16].upper()}' + req = PaymentRequest( + OutTradeNO=out_trade_no, + TotalAmount=Decimal(str(amount)), + Subject=subject, + Body=body, + Method=method, + OpenID=open_id, + ClientIP=client_ip, + ExpireMinutes=expire_minutes, + Extra=extra or {}, + ) + + tx = PaymentTransaction( + TransactionUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + Platform=platform, + TransactionType=PaymentTransaction.TX_PAY, + OutTradeNO=out_trade_no, + TotalAmount=Decimal(str(amount)), + Subject=subject, + RawRequest={ + 'method': method, + 'open_id': open_id, + 'extra': extra or {}, + }, + ) + tx.save() + + resp = gateway.Pay(req) + tx.RawResponse = resp.RawResponse + tx.UpdateTime = datetime.datetime.utcnow() + + if resp.Success: + tx.ChannelTransactionID = resp.TransactionID + tx.save(update_fields=['RawResponse', 'ChannelTransactionID', 'UpdateTime']) + tx._prepay_info = resp.PayInfo + return tx + else: + tx.MarkFailed() + raise PaymentError('支付发起失败', transaction=tx) + + def Query(self, out_trade_no, platform=None): + """查询订单状态 + + Args: + out_trade_no: 商户订单号 + platform: 支付平台(不传则从交易记录推断) + + Returns: + QueryResponse + """ + try: + tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no) + except PaymentTransaction.DoesNotExist: + raise PaymentError(f'交易记录不存在: {out_trade_no}') + + platform = platform or tx.Platform + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + raise PaymentError(f'租户未配置支付渠道: {platform}') + + creds = self._build_credentials(config) + gateway = payment_registry.create(platform, creds) + result = gateway.Query(out_trade_no) + + if result.IsPaid and not tx.IsSuccess: + tx.MarkSuccess( + channel_transaction_id=result.TransactionID, + notify_data={'query_result': result.RawResponse}, + ) + elif result.IsRefunded: + tx.TransactionStatus = PaymentTransaction.TX_CLOSED + tx.RawResponse = result.RawResponse + tx.UpdateTime = datetime.datetime.utcnow() + tx.save(update_fields=['TransactionStatus', 'RawResponse', 'UpdateTime']) + + return result + + def Refund(self, out_trade_no, amount, reason='', platform=None, extra=None): + """发起退款 + + Args: + out_trade_no: 商户订单号 + amount: 退款金额 + reason: 退款原因 + platform: 支付平台 + extra: 额外参数 + + Returns: + PaymentTransaction 实例 + """ + try: + pay_tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no, TransactionType=PaymentTransaction.TX_PAY) + except PaymentTransaction.DoesNotExist: + raise PaymentError(f'原始交易不存在: {out_trade_no}') + + if not pay_tx.IsSuccess: + raise PaymentError(f'原始交易未支付成功,无法退款: {out_trade_no}') + + platform = platform or pay_tx.Platform + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + raise PaymentError(f'租户未配置支付渠道: {platform}') + + creds = self._build_credentials(config) + gateway = payment_registry.create(platform, creds) + + out_refund_no = f'RF-{uuid.uuid4().hex[:16].upper()}' + amount = Decimal(str(amount)) + + if amount > pay_tx.TotalAmount: + raise PaymentError(f'退款金额超过原始交易金额: {amount} > {pay_tx.TotalAmount}') + + req = RefundRequest( + OutTradeNO=out_trade_no, + OutRefundNO=out_refund_no, + TotalAmount=pay_tx.TotalAmount, + RefundAmount=amount, + Reason=reason, + Extra=extra or {}, + ) + + rx = PaymentTransaction( + TransactionUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + Platform=platform, + TransactionType=PaymentTransaction.TX_REFUND, + OutTradeNO=out_trade_no, + OutRefundNO=out_refund_no, + TotalAmount=amount, + RawRequest={'refund_reason': reason, 'extra': extra or {}}, + ) + rx.save() + + resp = gateway.Refund(req) + rx.RawResponse = resp.RawResponse + + if resp.Success: + rx.MarkSuccess(channel_transaction_id=resp.RefundID) + else: + rx.MarkFailed() + + return rx + + def Transfer(self, platform, amount, payee_account='', + payee_name='', description='', transfer_type='wallet', + extra=None): + """企业付款到零钱/银行卡 + + Args: + platform: 支付平台 + amount: 转账金额 + payee_account: 收款账户 + payee_name: 收款人姓名 + description: 转账备注 + transfer_type: 转账类型 (wallet/bank) + extra: 额外参数 + + Returns: + PaymentTransaction 实例 + """ + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + raise PaymentError(f'租户未配置支付渠道: {platform}') + + creds = self._build_credentials(config) + gateway = payment_registry.create(platform, creds) + + out_transfer_no = f'TF-{uuid.uuid4().hex[:16].upper()}' + amount = Decimal(str(amount)) + + req = TransferRequest( + OutTransferNO=out_transfer_no, + Amount=amount, + PayeeAccount=payee_account, + PayeeName=payee_name, + Description=description, + TransferType=transfer_type, + Extra=extra or {}, + ) + + tx = PaymentTransaction( + TransactionUUID=uuid.uuid4().bytes, + TenantUUID=self.TenantUUID, + Platform=platform, + TransactionType=PaymentTransaction.TX_TRANSFER, + OutTradeNO=out_transfer_no, + TotalAmount=amount, + Subject=description, + RawRequest={ + 'payee_account': payee_account, + 'payee_name': payee_name, + 'transfer_type': transfer_type, + 'extra': extra or {}, + }, + ) + tx.save() + + try: + resp = gateway.Transfer(req) + except NotImplementedError as e: + tx.MarkFailed(raw_response={'error': str(e)}) + raise PaymentError(str(e), transaction=tx) + + tx.RawResponse = resp.RawResponse + if resp.Success: + tx.MarkSuccess(channel_transaction_id=resp.TransferID) + else: + tx.MarkFailed() + + return tx + + def HandleNotify(self, platform, raw_data, headers=None): + """处理支付回调通知 + + Args: + platform: 支付平台 + raw_data: 原始回调数据 (bytes) + headers: 请求头字典 + + Returns: + PaymentTransaction 实例或 None + """ + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + logger.error(f'租户未配置支付渠道: {platform}') + return None + + creds = self._build_credentials(config) + gateway = payment_registry.create(platform, creds) + + if not gateway.VerifyNotify(raw_data, headers or {}): + logger.warning(f'支付回调签名验证失败: {platform}') + return None + + notify_data = gateway.ParseNotify(raw_data) + out_trade_no = notify_data.get('out_trade_no', '') + if not out_trade_no: + logger.warning('回调缺少 out_trade_no') + return None + + try: + tx = PaymentTransaction.objects.get(OutTradeNO=out_trade_no) + except PaymentTransaction.DoesNotExist: + logger.warning(f'回调对应交易不存在: {out_trade_no}') + return None + + trade_state = notify_data.get('trade_state', '') + transaction_id = notify_data.get('transaction_id', '') + + if trade_state in ('SUCCESS', 'TRADE_SUCCESS', 'TRADE_FINISHED'): + tx.MarkSuccess( + channel_transaction_id=transaction_id, + notify_data=notify_data, + ) + elif trade_state in ('CLOSED', 'REVOKED', 'TRADE_CLOSED'): + tx.MarkClosed() + elif trade_state in ('REFUND', 'TRADE_REFUND'): + tx.TransactionStatus = PaymentTransaction.TX_CLOSED + tx.NotifyData = notify_data + tx.UpdateTime = datetime.datetime.utcnow() + tx.save(update_fields=['TransactionStatus', 'NotifyData', 'UpdateTime']) + + return tx + + def GetGatewayInstance(self, platform): + """直接获取网关实例——供子服务自定义操作 + + Args: + platform: 支付平台 + + Returns: + AbstractPaymentGateway 实例 + + Raises: + PaymentError: 租户未配置支付渠道 + """ + config = PaymentGatewayConfig.FromTenantAndPlatform(self.TenantUUID, platform) + if config is None: + raise PaymentError(f'租户未配置支付渠道: {platform}') + + creds = self._build_credentials(config) + return payment_registry.create(platform, creds) + + def _build_credentials(self, config): + """从数据库配置构建 PaymentCredentials""" + return PaymentCredentials( + AppID=config.AppID, + MchID=config.MchID, + APIKey=config.DecryptAPIKey(), + PrivateKey=config.DecryptPrivateKey(), + PublicKey=config.DecryptPublicKey(), + CertPath=config.CertPath, + CertKeyPath=config.CertKeyPath, + NotifyURL=config.NotifyURL, + ReturnURL=config.ReturnURL, + Extra=config.ConfigExtra, + ) diff --git a/gvsdsdk/payment/models.py b/gvsdsdk/payment/models.py new file mode 100644 index 0000000..5974c5a --- /dev/null +++ b/gvsdsdk/payment/models.py @@ -0,0 +1,194 @@ +"""模块:支付 SAAS 数据模型 +SAAS 级支付配置与交易流水——不绑定具体业务。 +子服务自行关联 payment_transaction_id 到自己的订单。""" + +import uuid +import datetime +from django.db import models +from gvsdsdk.model_base import QModel +from gvsdsdk.models import UUIDField + + +class PaymentGatewayConfig(QModel): + """支付网关配置表——每个租户每个渠道一条配置 + + 存储加密后的支付密钥,子服务通过此表配置自己的支付能力。 + SAAS 主服务器不关心具体业务,只提供通道。 + """ + + ConfigUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + Platform = models.CharField(max_length=32) + AppID = models.CharField(max_length=64, default='') + MchID = models.CharField(max_length=64, default='') + APIKeyEncrypted = models.CharField(max_length=512, default='') + PrivateKeyEncrypted = models.TextField(default='') + PublicKeyEncrypted = models.TextField(default='') + CertPath = models.CharField(max_length=512, default='') + CertKeyPath = models.CharField(max_length=512, default='') + NotifyURL = models.CharField(max_length=512, default='') + ReturnURL = models.CharField(max_length=512, default='') + ConfigExtra = models.JSONField(default=dict) + ConfigStatus = models.SmallIntegerField(default=1) + ConfigDefault = models.BooleanField(default=False) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + UpdateTime = models.DateTimeField(null=True, blank=True) + + class Meta: + db_table = 'PaymentGatewayConfig' + app_label = 'gvsdsdk' + verbose_name = '支付网关配置' + verbose_name_plural = '支付网关配置' + unique_together = [('TenantUUID', 'Platform')] + managed = False + indexes = [ + models.Index(fields=['TenantUUID'], name='idx_paycfg_tenant'), + models.Index(fields=['Platform'], name='idx_paycfg_platform'), + ] + + def __str__(self): + return f'{self.Platform}@{self.TenantUUID}' + + @property + def IsActive(self): + return self.ConfigStatus == 1 + + def DecryptAPIKey(self): + if not self.APIKeyEncrypted: + return '' + return _decrypt_field(self.APIKeyEncrypted) + + def DecryptPrivateKey(self): + if not self.PrivateKeyEncrypted: + return '' + return _decrypt_field(self.PrivateKeyEncrypted) + + def DecryptPublicKey(self): + if not self.PublicKeyEncrypted: + return '' + return _decrypt_field(self.PublicKeyEncrypted) + + @classmethod + def FromTenantAndPlatform(cls, tenant_uuid, platform): + try: + return cls.objects.get(TenantUUID=tenant_uuid, Platform=platform, ConfigStatus=1) + except cls.DoesNotExist: + return None + + +class PaymentTransaction(QModel): + """支付交易流水表——SAAS 级统一交易记录 + + 每笔支付/退款/转账操作都在此记录一条流水。 + 子服务通过此表的 TransactionUUID 关联自己的业务订单。 + """ + + TX_PAY = 'pay' + TX_REFUND = 'refund' + TX_TRANSFER = 'transfer' + + TX_TYPE_CHOICES = [ + (TX_PAY, '支付'), + (TX_REFUND, '退款'), + (TX_TRANSFER, '转账'), + ] + + TX_PENDING = 0 + TX_SUCCESS = 1 + TX_FAILED = 2 + TX_CLOSED = 3 + + TX_STATUS_CHOICES = [ + (TX_PENDING, '处理中'), + (TX_SUCCESS, '成功'), + (TX_FAILED, '失败'), + (TX_CLOSED, '已关闭'), + ] + + TransactionUUID = UUIDField(primary_key=True) + TenantUUID = UUIDField() + Platform = models.CharField(max_length=32) + TransactionType = models.CharField(max_length=16, choices=TX_TYPE_CHOICES) + OutTradeNO = models.CharField(max_length=64, unique=True) + OutRefundNO = models.CharField(max_length=64, default='') + ChannelTransactionID = models.CharField(max_length=128, default='') + TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0) + Currency = models.CharField(max_length=10, default='CNY') + TransactionStatus = models.SmallIntegerField( + default=TX_PENDING, choices=TX_STATUS_CHOICES, + ) + Subject = models.CharField(max_length=256, default='') + RawRequest = models.JSONField(default=dict) + RawResponse = models.JSONField(default=dict) + NotifyData = models.JSONField(default=dict) + PayTime = models.DateTimeField(null=True, blank=True) + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow) + UpdateTime = models.DateTimeField(null=True, blank=True) + + class Meta: + db_table = 'PaymentTransaction' + app_label = 'gvsdsdk' + verbose_name = '支付交易流水' + verbose_name_plural = '支付交易流水' + ordering = ['-CreateTime'] + managed = False + indexes = [ + models.Index(fields=['TenantUUID', 'CreateTime'], name='idx_paytx_tenant_time'), + models.Index(fields=['Platform', 'TransactionStatus'], name='idx_paytx_platform_status'), + models.Index(fields=['OutTradeNO'], name='idx_paytx_out_trade_no'), + ] + + def __str__(self): + return self.OutTradeNO + + @property + def IsSuccess(self): + return self.TransactionStatus == self.TX_SUCCESS + + def MarkSuccess(self, channel_transaction_id='', pay_time=None, notify_data=None): + self.TransactionStatus = self.TX_SUCCESS + self.ChannelTransactionID = channel_transaction_id + self.PayTime = pay_time or datetime.datetime.utcnow() + if notify_data: + self.NotifyData = notify_data + self.UpdateTime = datetime.datetime.utcnow() + self.save(update_fields=[ + 'TransactionStatus', 'ChannelTransactionID', + 'PayTime', 'NotifyData', 'UpdateTime', + ]) + + def MarkFailed(self, raw_response=None): + self.TransactionStatus = self.TX_FAILED + if raw_response: + self.RawResponse = raw_response + self.UpdateTime = datetime.datetime.utcnow() + self.save(update_fields=['TransactionStatus', 'RawResponse', 'UpdateTime']) + + def MarkClosed(self): + self.TransactionStatus = self.TX_CLOSED + self.UpdateTime = datetime.datetime.utcnow() + self.save(update_fields=['TransactionStatus', 'UpdateTime']) + + +def _decrypt_field(encrypted_value): + import base64 + from django.conf import settings + key = getattr(settings, 'PAYMENT_ENCRYPTION_KEY', 'SMANAGE-payment-default-key-32b') + try: + raw = base64.b64decode(encrypted_value) + return bytes(a ^ b for a, b in zip(raw, key.encode() * (len(raw) // len(key.encode()) + 1))).decode('utf-8') + except Exception: + return encrypted_value + + +def EncryptField(plain_value): + import base64 + from django.conf import settings + if not plain_value: + return '' + key = getattr(settings, 'PAYMENT_ENCRYPTION_KEY', 'SMANAGE-payment-default-key-32b') + raw = bytes(a ^ b for a, b in zip( + plain_value.encode('utf-8'), + key.encode() * (len(plain_value.encode('utf-8')) // len(key.encode()) + 1), + )) + return base64.b64encode(raw).decode('utf-8') diff --git a/gvsdsdk/pyproject.toml b/gvsdsdk/pyproject.toml new file mode 100644 index 0000000..5bfb1c2 --- /dev/null +++ b/gvsdsdk/pyproject.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "gvsdsdk" +version = "1.0.0" +description = "GVS-DSDK — GVS Distributed SDK, shared communication bridge between gvsds master and sub-servers" +readme = "README.md" +requires-python = ">=3.8" +license = { text = "Proprietary" } +authors = [ + { name = "GVS Team" }, +] +keywords = ["gvsds", "msync", "distributed", "sdk", "service-mesh"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: Other/Proprietary License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +# 核心依赖:仅标准库 + gvsds 原生层 +dependencies = [ + # 纯标准库实现,运行时无第三方依赖 + # 仅开发时需要:pytest(可选) +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", +] +client = [ + # 子服务器需要 socketio + websocket-client + "python-socketio[client]>=5.0,<6.0", + "websocket-client>=1.5", + "requests>=2.28", +] +master = [ + # 主服务器需要 socketio 服务端 + "python-socketio[asyncio]>=5.0,<6.0", + "gevent>=22.0", + "gevent-websocket>=0.10", + "eventlet>=0.33", +] + +[project.urls] +Homepage = "https://www.gvsds.com" +Documentation = "https://www.gvsds.com/docs/sdk" +Repository = "https://github.com/gvsds/gvsdsdk" +Issues = "https://github.com/gvsds/gvsdsdk/issues" + +[project.scripts] +gvsdsdk = "gvsdsdk.__main__:main" + +[tool.setuptools] +# 显式列出模块文件,避免命名混乱 +# 包含 SDK 模块(Windows 下文件名不区分大小写,但内容按原始名导入) +py-modules = [ + "gvsdsdk.auth", + "gvsdsdk.registry", + "gvsdsdk.ogm", + "gvsdsdk.Transaction", + "gvsdsdk.LocalTx", + "gvsdsdk.client", + "gvsdsdk.models", +] + +[tool.setuptools.package-data] +gvsdsdk = ["README.md", "pyproject.toml"] \ No newline at end of file diff --git a/orders/models.py b/orders/models.py index 8371ff0..77321ff 100644 --- a/orders/models.py +++ b/orders/models.py @@ -1,7 +1,8 @@ from django.db import models +from gvsdsdk.model_base import QModel -class Dingdan(models.Model): +class Dingdan(QModel): """订单主表""" # 核心编号:唯一且不可为空[citation:3][citation:9] dingdan_id = models.CharField(max_length=32, unique=True,db_index=True, verbose_name='订单ID') @@ -65,7 +66,7 @@ class Dingdan(models.Model): def __str__(self): return self.dingdan_id -class DingdanPingtai(models.Model): +class DingdanPingtai(QModel): """平台订单扩展表 (当 fadan_pingtai=1 时使用)""" dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='pingtai_kuozhan', verbose_name='订单') laoban_id = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='老板ID') @@ -98,7 +99,7 @@ class DingdanPingtai(models.Model): return f"平台扩展-{self.dingdan.dingdan_id}" -class DingdanShangjia(models.Model): +class DingdanShangjia(QModel): """商家发单扩展表 (当 fadan_pingtai=2 时使用)""" dingdan = models.OneToOneField(Dingdan, on_delete=models.CASCADE, related_name='shangjia_kuozhan', verbose_name='订单') shangjia_id = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='商家ID') @@ -135,7 +136,7 @@ class DingdanShangjia(models.Model): -class DingdanPeihu(models.Model): +class DingdanPeihu(QModel): """陪护订单表(一个订单最多2个陪护)""" # 关联订单(一对一,陪护订单依附于原订单) dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='订单ID') @@ -180,7 +181,7 @@ class DingdanPeihu(models.Model): -class Fadan(models.Model): +class Fadan(QModel): """罚单表——平台各类处罚信息""" id = models.AutoField(primary_key=True, verbose_name='自增主键') beichufa_id = models.CharField(max_length=20, db_index=True, verbose_name='被处罚用户ID') # 直接用yonghuid,后续可改为外键 @@ -228,7 +229,7 @@ class Fadan(models.Model): # ==================== 罚款申诉图片表 ==================== -class FadanShensuTupian(models.Model): +class FadanShensuTupian(QModel): """罚款申诉图片,关联罚单""" fadan = models.ForeignKey(Fadan, on_delete=models.CASCADE, related_name='shensu_tupians', verbose_name='关联罚单') beichufa_id = models.CharField(max_length=7, db_index=True, verbose_name='被处罚用户ID') # 冗余,便于直接按用户查 @@ -253,7 +254,7 @@ class FadanShensuTupian(models.Model): # 与 Fadan 放在同一个文件或独立的 fadan/models.py -class FadanFenhong(models.Model): +class FadanFenhong(QModel): """罚款分红表——记录罚款产生的分红信息""" id = models.AutoField(primary_key=True, verbose_name='自增主键') fenhongzhe_id = models.CharField(max_length=20, db_index=True, verbose_name='分红者ID') @@ -282,7 +283,7 @@ class FadanFenhong(models.Model): -class FadanFenhongLilv(models.Model): +class FadanFenhongLilv(QModel): shenfen = models.IntegerField(primary_key=True, choices=[(1, '客服'), (2, '售后'), (3, '管理员'), (4, '店铺'), (5, '商家')], verbose_name='申请处罚人身份') lilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.0000, verbose_name='分红费率') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') @@ -299,7 +300,7 @@ class FadanFenhongLilv(models.Model): -class OrderDashouHistory(models.Model): +class OrderDashouHistory(QModel): """ 订单打手变更历史表 记录订单每次更换打手的操作 @@ -327,7 +328,7 @@ class OrderDashouHistory(models.Model): -class Dashoutupian(models.Model): +class Dashoutupian(QModel): dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') dashou = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='接单打手ID') tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='打手提交图片URL') @@ -344,7 +345,7 @@ class Dashoutupian(models.Model): -class Pingfen(models.Model): +class Pingfen(QModel): dashou = models.CharField(max_length=32, null=True, blank=True, db_index=True,verbose_name='接单打手ID') zongfen = models.PositiveIntegerField(default=0, verbose_name='总分') cishu = models.PositiveIntegerField(default=0, verbose_name='评分次数') @@ -368,7 +369,7 @@ class Pingfen(models.Model): -class Lilubiao(models.Model): +class Lilubiao(QModel): lilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='平台分成利率,1是平台,2是商家') #sahngjialilu = models.DecimalField(max_digits=5, decimal_places=4,null=True, verbose_name='商家利率') fadanpingtai = models.CharField(max_length=10,null=True,verbose_name='订单所属平台,(商家或平台)') @@ -380,7 +381,7 @@ class Lilubiao(models.Model): -class Chufajilu(models.Model): +class Chufajilu(QModel): dashouid = models.CharField(max_length=7, null=True, db_index=True, verbose_name='被罚打手ID') qingqiuid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='请求处罚ID') chuliid = models.CharField(max_length=11, null=True, db_index=True, verbose_name='处理处罚ID') @@ -402,7 +403,7 @@ class Chufajilu(models.Model): ] -class Chufatupian(models.Model): +class Chufatupian(QModel): dingdan_id = models.CharField(max_length=32,db_index=True, null=True, verbose_name='订单ID') yonghuid = models.CharField(max_length=32, null=True, blank=True,db_index=True, verbose_name='用户ID') tupian = models.CharField(max_length=500, null=True, blank=True, verbose_name='证据图片URL') @@ -418,7 +419,7 @@ class Chufatupian(models.Model): ] -class Tuikuanjilu(models.Model): +class Tuikuanjilu(QModel): dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') dashouid = models.CharField(max_length=7, null=True, verbose_name='接单打手ID') qingqiuid = models.CharField(max_length=7, null=True, verbose_name='请求用户ID') @@ -442,7 +443,7 @@ class Tuikuanjilu(models.Model): ] -class Ptliushui(models.Model): +class Ptliushui(QModel): zonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='收取总金额') zhichuzonge = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='支出总金额') zongshouyi = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='总收益') @@ -457,7 +458,7 @@ class Ptliushui(models.Model): db_table = 'ptliushui' -class Liaotian(models.Model): +class Liaotian(QModel): """聊天关系表 - 极简设计,不设置唯一约束""" # 主键 id = models.BigAutoField(primary_key=True, verbose_name='主键ID') diff --git a/orders/signals.py b/orders/signals.py index 9a94849..4dbdae9 100644 --- a/orders/signals.py +++ b/orders/signals.py @@ -10,6 +10,7 @@ from django.db.models.signals import pre_save from django.dispatch import receiver from django.conf import settings from django.utils import timezone +from gvsdsdk.fluent import db, func, FQ logger = logging.getLogger(__name__) @@ -52,7 +53,7 @@ def handle_order_status_8(sender, instance, **kwargs): from orders.models import Dingdan # 获取旧状态 - old = Dingdan.objects.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() + old = Dingdan.query.filter(pk=instance.pk).values_list('zhuangtai', flat=True).first() # 状态从 非8 变为 8 if old != 8 and instance.zhuangtai == 8: diff --git a/orders/tasks.py b/orders/tasks.py index 0e426c6..04796c9 100644 --- a/orders/tasks.py +++ b/orders/tasks.py @@ -17,6 +17,7 @@ import logging from orders.models import Dingdan from users.models import UserMain, UserDashou, UserShangjia from utils.celery_utils import safe_decimal_operation, log_task_execution, rollback_on_failure +from gvsdsdk.fluent import db, func, FQ logger = logging.getLogger(__name__) @@ -62,13 +63,13 @@ def process_expired_order(self, dingdan_id): if dashou_id: try: # 查询打手用户主表 - dashou_user = UserMain.objects.filter(yonghuid=dashou_id).first() + dashou_user = UserMain.query.filter(yonghuid=dashou_id).first() if dashou_user: # 获取打手扩展表 dashou_profile = getattr(dashou_user, 'dashou_profile', None) if dashou_profile: # 使用F表达式原子更新 - UserDashou.objects.filter(pk=dashou_profile.pk).update( + UserDashou.query.filter(pk=dashou_profile.pk).update( chengjiaozongliang=F('chengjiaozongliang') + 1, yue=F('yue') + dashou_fencheng, zonge=F('zonge') + dashou_fencheng, @@ -91,12 +92,12 @@ def process_expired_order(self, dingdan_id): shangjia_id = shangjia_kuozhan.shangjia_id if shangjia_id: # 查询商家用户 - shangjia_user = UserMain.objects.filter(yonghuid=shangjia_id).first() + shangjia_user = UserMain.query.filter(yonghuid=shangjia_id).first() if shangjia_user: shangjia_profile = getattr(shangjia_user, 'shop_profile', None) if shangjia_profile: # 更新商家成交订单数量 - UserShangjia.objects.filter(pk=shangjia_profile.pk).update( + UserShangjia.query.filter(pk=shangjia_profile.pk).update( chengjiao=F('chengjiao') + 1 ) updated_shangjia = True @@ -151,7 +152,7 @@ def check_order_expire_task(): check_time = timezone.now() - timedelta(seconds=settings.ORDER_EXPIRE_SECONDS + 86400) # 48+24=72小时 # 查询状态为8且创建时间早于检查时间的订单 - expired_orders = Dingdan.objects.filter( + expired_orders = Dingdan.query.filter( zhuangtai=8, create_time__lt=check_time ).values_list('dingdan_id', flat=True) diff --git a/orders/views.py b/orders/views.py index 5935591..e75b63e 100644 --- a/orders/views.py +++ b/orders/views.py @@ -15,7 +15,8 @@ import logging from decimal import Decimal from django.conf import settings -from django.db import models, transaction +from django.db import models, transaction +from gvsdsdk.fluent import db, func, FQ from django.db.models import F, Q, OuterRef, Subquery from django.core.cache import cache from django.core.exceptions import ObjectDoesNotExist @@ -66,11 +67,10 @@ from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChengha from config.models import ( Szjilu, ShangjiaLianjie ) + + logger = logging.getLogger(__name__) - - - class PaymentFailView(APIView): """ 前端支付失败接口 - 处理用户主动取消或支付失败 @@ -98,7 +98,7 @@ class PaymentFailView(APIView): return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None}) # 删除扩展表和主表 - DingdanPingtai.objects.filter(dingdan=dingdan).delete() + DingdanPingtai.query.filter(dingdan=dingdan).delete() dingdan.delete() return Response({'code': 0, 'msg': '订单已删除', 'data': None}) @@ -225,7 +225,7 @@ class WechatPayNotifyView(APIView): # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25) try: - abandoned_count = Dingdan.objects.filter( + abandoned_count = Dingdan.query.filter( user1_id=dingdan.user1_id, shangpin_id=dingdan.shangpin_id, zhuangtai=9 @@ -240,7 +240,7 @@ class WechatPayNotifyView(APIView): if hasattr(dingdan, 'pingtai_kuozhan'): laoban_id = dingdan.pingtai_kuozhan.laoban_id if laoban_id: - user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + user_main = UserMain.query.filter(yonghuid=laoban_id).first() if user_main and hasattr(user_main, 'boss_profile'): boss = user_main.boss_profile boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) @@ -253,7 +253,7 @@ class WechatPayNotifyView(APIView): # 更新商品库存 try: if dingdan.shangpin_id: - Shangpin.objects.filter(id=dingdan.shangpin_id).update( + Shangpin.query.filter(id=dingdan.shangpin_id).update( kucun=F('kucun') - 1 ) logger.info(f"商品 {dingdan.shangpin_id} 库存减1") @@ -401,7 +401,7 @@ class WechatPayNotifyView(APIView): def _get_game_type_name(self, leixing_id): try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: logger.error(f"获取游戏类型名称异常: {e}") @@ -426,7 +426,7 @@ class PaymentVerifyView(APIView): return Response({'code': 1, 'msg': '订单ID不能为空'}) # ---------- 2. 查询订单 ---------- - dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first() + dingdan = Dingdan.query.filter(dingdan_id=dingdanid).first() if not dingdan: return Response({'code': 2, 'msg': '订单不存在'}) @@ -515,7 +515,7 @@ class PaymentVerifyView(APIView): dingdan.save() # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25) - Dingdan.objects.filter( + Dingdan.query.filter( user1_id=dingdan.user1_id, shangpin_id=dingdan.shangpin_id, zhuangtai=9 @@ -524,7 +524,7 @@ class PaymentVerifyView(APIView): # 更新老板扩展表 laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None if laoban_id: - user_main = UserMain.objects.filter(yonghuid=laoban_id).first() + user_main = UserMain.query.filter(yonghuid=laoban_id).first() if user_main and hasattr(user_main, 'boss_profile'): boss = user_main.boss_profile boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0) @@ -533,7 +533,7 @@ class PaymentVerifyView(APIView): # 更新商品库存 if dingdan.shangpin_id: - Shangpin.objects.filter(id=dingdan.shangpin_id).update( + Shangpin.query.filter(id=dingdan.shangpin_id).update( kucun=F('kucun') - 1 ) @@ -686,7 +686,7 @@ class CreateOrderView(APIView): return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None}) # 3. 查询商品信息 - shangpin = Shangpin.objects.filter(id=shangpin_id).first() + shangpin = Shangpin.query.filter(id=shangpin_id).first() if not shangpin: return Response({'code': 4, 'msg': '商品不存在', 'data': None}) @@ -710,7 +710,7 @@ class CreateOrderView(APIView): zhiding_id_value = None if zhiding: # 查询主表中是否存在该打手 - zhiding_user = UserMain.objects.filter( + zhiding_user = UserMain.query.filter( yonghuid=zhiding, #user_type='dashou' ).first() @@ -719,7 +719,7 @@ class CreateOrderView(APIView): return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) # 查询打手扩展表 - dashou_profile = UserDashou.objects.filter(user=zhiding_user).first() + dashou_profile = UserDashou.query.filter(user=zhiding_user).first() if not dashou_profile: return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) @@ -738,7 +738,7 @@ class CreateOrderView(APIView): # 6. 查询利率并计算分成 # 查询打手利率(字符'1') - dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first() + dashou_lilu = Lilubiao.query.filter(fadanpingtai='1').first() # 🔥 新增:判断是否使用商品配置的额外分成 @@ -751,7 +751,7 @@ class CreateOrderView(APIView): # 查询管事利率(字符'2') - #guanshi_lilu = Lilubiao.objects.filter(fadanpingtai='2').first() + #guanshi_lilu = Lilubiao.query.filter(fadanpingtai='2').first() if not dashou_lilu: #or not guanshi_lilu: return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) @@ -792,7 +792,7 @@ class CreateOrderView(APIView): # 创建订单记录 - dingdan = Dingdan.objects.create(**order_data) + dingdan = Dingdan.query.create(**order_data) # CreateOrderView.post 事务内部,替换原有的扩展表创建代码 @@ -817,7 +817,7 @@ class CreateOrderView(APIView): else: pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') - DingdanPingtai.objects.create(**pingtaikuozhan_data) + DingdanPingtai.query.create(**pingtaikuozhan_data) # 计算收益 @@ -840,7 +840,7 @@ class CreateOrderView(APIView): # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0 pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') - DingdanPingtai.objects.create(**pingtaikuozhan_data)''' + DingdanPingtai.query.create(**pingtaikuozhan_data)''' # 7. 生成微信支付参数(向微信官方发送请求) @@ -1055,7 +1055,7 @@ class DingdanHuoquView(APIView): try: # 查询扩展表,获取用户的所有订单ID - pingtai_orders = DingdanPingtai.objects.filter(laoban_id=yonghuid) + pingtai_orders = DingdanPingtai.query.filter(laoban_id=yonghuid) pingtai_order_ids = list(pingtai_orders.values_list('dingdan__dingdan_id', flat=True)) if not pingtai_order_ids: @@ -1075,7 +1075,7 @@ class DingdanHuoquView(APIView): status_query = Q(zhuangtai__in=zhuangtai_list) query = Q(dingdan_id__in=pingtai_order_ids) & status_query - orders = Dingdan.objects.filter(query).order_by('-create_time') + orders = Dingdan.query.filter(query).order_by('-create_time') #total_orders = orders.count() # 手动分页逻辑 @@ -1164,14 +1164,14 @@ class DingdanXiangqingView(APIView): try: # 1. 验证订单是否属于当前用户 # 查询平台订单扩展表,检查laoban_id是否等于当前用户 - pingtai_kuozhan = DingdanPingtai.objects.filter( + pingtai_kuozhan = DingdanPingtai.query.filter( laoban_id=yonghuid, dingdan__dingdan_id=dingdan_id ).first() if not pingtai_kuozhan: # 如果不是平台订单,检查商家订单 - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( shangjia_id=yonghuid, dingdan__dingdan_id=dingdan_id ).first() @@ -1184,7 +1184,7 @@ class DingdanXiangqingView(APIView): }) # 2. 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + dingdan = Dingdan.query.filter(dingdan_id=dingdan_id).first() if not dingdan: return Response({ @@ -1200,7 +1200,7 @@ class DingdanXiangqingView(APIView): dashou_liuyan = dingdan.dashou_liuyan or '' # 4. 查询打手交付图片 - dashoutupian_records = Dashoutupian.objects.filter( + dashoutupian_records = Dashoutupian.query.filter( dingdan_id=dingdan_id ).order_by('create_time')[:9] # 最多取9条 @@ -1283,7 +1283,7 @@ class JiedanView(APIView): try: with transaction.atomic(): # 1. 验证订单是否存在且属于当前用户 - pingtai_kuozhan = DingdanPingtai.objects.filter( + pingtai_kuozhan = DingdanPingtai.query.filter( laoban_id=yonghuid, dingdan__dingdan_id=dingdan_id ).select_related('dingdan').first() @@ -1318,7 +1318,7 @@ class JiedanView(APIView): # 5. 如果有评分,更新评分表 if pingfen > 0 and jiedan_dashou_id: # 使用filter查询,更健壮 - pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + pingfen_records = Pingfen.query.filter(dashou=jiedan_dashou_id) if pingfen_records.exists(): # 已有记录,更新 @@ -1328,7 +1328,7 @@ class JiedanView(APIView): pingfen_record.save() else: # 新记录,创建 - Pingfen.objects.create( + Pingfen.query.create( dashou=jiedan_dashou_id, zongfen=Decimal(str(pingfen)), cishu=1 @@ -1336,7 +1336,7 @@ class JiedanView(APIView): # 6. 更新商品真实销量(如果有商品ID) if shangpin_id: - shangpin_records = Shangpin.objects.filter(id=shangpin_id) + shangpin_records = Shangpin.query.filter(id=shangpin_id) if shangpin_records.exists(): shangpin = shangpin_records.first() @@ -1348,11 +1348,11 @@ class JiedanView(APIView): # 7. 更新打手扩展表(如果有打手ID和分成) if jiedan_dashou_id and dashou_fencheng > 0: # 查询打手用户 - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = UserMain.query.filter(yonghuid=jiedan_dashou_id).first() if dashou_user: # 查询打手扩展表 - dashou_kuozhan = UserDashou.objects.filter(user=dashou_user).first() + dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first() if dashou_kuozhan: # 更新四个金额字段 @@ -1484,7 +1484,7 @@ class DingdanXiangqingView2(APIView): try: # 权限验证 - pingtai_ext = DingdanPingtai.objects.filter( + pingtai_ext = DingdanPingtai.query.filter( laoban_id=yonghuid, dingdan__dingdan_id=dingdan_id ).select_related('dingdan').first() @@ -1500,7 +1500,7 @@ class DingdanXiangqingView2(APIView): # 交付图片 dashoujiaofu_list = list( - Dashoutupian.objects.filter(dingdan_id=dingdan_id) + Dashoutupian.query.filter(dingdan_id=dingdan_id) .order_by('create_time')[:9] .values_list('tupian', flat=True) ) @@ -1511,14 +1511,14 @@ class DingdanXiangqingView2(APIView): jiedan_time = '' tijiao_time = '' if jiedan_dashou_id: - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = UserMain.query.filter(yonghuid=jiedan_dashou_id).first() if dashou_user: dashou_touxiang = dashou_user.avatar or '' try: dashou_nicheng = dashou_user.dashou_profile.nicheng or '' except UserDashou.DoesNotExist: pass - biaoxian = DashouBiaoxian.objects.filter( + biaoxian = DashouBiaoxian.query.filter( dingdan_id=dingdan_id, dashou_id=jiedan_dashou_id ).first() @@ -1534,7 +1534,7 @@ class DingdanXiangqingView2(APIView): fuwu_taidu = None fanche_cishu = None if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价 - biaoxian = DashouBiaoxian.objects.filter( + biaoxian = DashouBiaoxian.query.filter( dingdan_id=dingdan_id, dashou_id=jiedan_dashou_id ).first() @@ -1610,7 +1610,7 @@ class JiedanView2(APIView): try: with transaction.atomic(): # 3. 权限验证:订单属于当前老板 - pingtai_ext = DingdanPingtai.objects.filter( + pingtai_ext = DingdanPingtai.query.filter( laoban_id=yonghuid, dingdan__dingdan_id=dingdan_id ).select_related('dingdan').first() @@ -1636,14 +1636,14 @@ class JiedanView2(APIView): # 7. 更新评分表(用服务态度分作为综合评分,保留原有统计) if jiedan_dashou_id and fuwu_taidu: - pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id) + pingfen_records = Pingfen.query.filter(dashou=jiedan_dashou_id) if pingfen_records.exists(): pf = pingfen_records.first() pf.zongfen += Decimal(str(fuwu_taidu)) pf.cishu += 1 pf.save() else: - Pingfen.objects.create( + Pingfen.query.create( dashou=jiedan_dashou_id, zongfen=Decimal(str(fuwu_taidu)), cishu=1 @@ -1651,7 +1651,7 @@ class JiedanView2(APIView): # 8. 更新商品真实销量 if shangpin_id: - shangpin_qs = Shangpin.objects.filter(id=shangpin_id) + shangpin_qs = Shangpin.query.filter(id=shangpin_id) if shangpin_qs.exists(): sp = shangpin_qs.first() if hasattr(sp, 'zhenshi_xiaoliang'): @@ -1660,7 +1660,7 @@ class JiedanView2(APIView): # 9. 更新打手收益与状态 if jiedan_dashou_id and dashou_fencheng > 0: - dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first() + dashou_user = UserMain.query.filter(yonghuid=jiedan_dashou_id).first() if dashou_user: try: dashou_ext = dashou_user.dashou_profile @@ -1756,7 +1756,7 @@ class ShangpinLeixingView(APIView): try: # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 - shangpin_queryset = ShangpinLeixing.objects.filter( + shangpin_queryset = ShangpinLeixing.query.filter( yaoqiuleixing=1, shenhezhuangtai=1 ).select_related('bankuai').order_by('paixu') @@ -1765,7 +1765,7 @@ class ShangpinLeixingView(APIView): bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) - chenghao_list = Chenghao.objects.filter( + chenghao_list = Chenghao.query.filter( bankuai_id__in=bankuai_ids ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') @@ -1853,7 +1853,7 @@ class ShangjiaPaifaView(APIView): # ----- 5. 商品类型 ----- try: - shangpin_type = ShangpinLeixing.objects.get(id=shangpin_type_id) + shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id) except ShangpinLeixing.DoesNotExist: logger.error(f"商品类型不存在: {shangpin_type_id}") return Response({'code': 400, 'msg': '商品类型不存在'}) @@ -1888,7 +1888,7 @@ class ShangjiaPaifaView(APIView): try: lid = int(label_id_str) if lid > 0: - chenghao_obj = Chenghao.objects.filter(id=lid).first() + chenghao_obj = Chenghao.query.filter(id=lid).first() if chenghao_obj: logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") else: @@ -1915,7 +1915,7 @@ class ShangjiaPaifaView(APIView): zhiding_dashou = None if zhiding_uid: try: - zhiding_user = UserMain.objects.get(yonghuid=zhiding_uid) + zhiding_user = UserMain.query.get(yonghuid=zhiding_uid) zhiding_dashou = zhiding_user.dashou_profile if zhiding_dashou.zhanghaozhuangtai != 1: return Response({'code': 400, 'msg': '指定打手状态异常'}) @@ -1938,7 +1938,7 @@ class ShangjiaPaifaView(APIView): dingdan = None try: with transaction.atomic(): - dingdan = Dingdan.objects.create( + dingdan = Dingdan.query.create( dingdan_id=generate_dingdan_id(), zhuangtai=7 if zhiding_dashou else 1, fadan_pingtai=2, @@ -1959,18 +1959,18 @@ class ShangjiaPaifaView(APIView): ) logger.info(f"订单主表创建: {dingdan.dingdan_id}") - DingdanShangjia.objects.create( + DingdanShangjia.query.create( dingdan=dingdan, shangjia_id=request.user.yonghuid, sjnicheng=shangjia.nicheng, ) if chenghao_obj: - DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=chenghao_obj) + DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj) logger.info(f"订单关联称号: {chenghao_obj.mingcheng}") # 更新商家统计 - UserShangjia.objects.filter(id=shangjia.id).update( + UserShangjia.query.filter(id=shangjia.id).update( fabu=F('fabu') + 1, yue=F('yue') - jiage, jinridingdan=F('jinridingdan') + 1, @@ -2012,7 +2012,7 @@ class ShangjiaPaifaView(APIView): def _get_game_type_name(self, leixing_id): try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception: return '游戏订单' @@ -2070,7 +2070,7 @@ class ShangjiaDingdanHuoquView(APIView): try: # 4. 构建基础查询(该商家的所有订单) - base_qs = DingdanShangjia.objects.filter( + base_qs = DingdanShangjia.query.filter( shangjia_id=yonghuid ).select_related('dingdan') @@ -2102,7 +2102,7 @@ class ShangjiaDingdanHuoquView(APIView): ) # 8. 计算待结算订单数量(状态为8) - pending_count = DingdanShangjia.objects.filter( + pending_count = DingdanShangjia.query.filter( shangjia_id=yonghuid, dingdan__zhuangtai=8 ).count() @@ -2196,7 +2196,7 @@ class ShangjiaDingdanXiangqingView(APIView): try: # 3. 查询商家订单扩展表,验证权限并获取基础订单数据 - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( shangjia_id=yonghuid, dingdan__dingdan_id=dingdan_id ).select_related('dingdan').first() @@ -2211,7 +2211,7 @@ class ShangjiaDingdanXiangqingView(APIView): order = shangjia_kuozhan.dingdan # 4. 交付图片列表 - dashou_tupian_list = Dashoutupian.objects.filter( + dashou_tupian_list = Dashoutupian.query.filter( dingdan_id=dingdan_id ).order_by('create_time').values_list('tupian', flat=True) dashou_jiaofu = list(dashou_tupian_list) @@ -2221,7 +2221,7 @@ class ShangjiaDingdanXiangqingView(APIView): dashou_nicheng = '' dashou_avatar = '' if dashou_yonghuid: - dashou_user = UserMain.objects.filter( + dashou_user = UserMain.query.filter( yonghuid=dashou_yonghuid ).select_related('dashou_profile').first() if dashou_user: @@ -2230,7 +2230,7 @@ class ShangjiaDingdanXiangqingView(APIView): dashou_nicheng = dashou_user.dashou_profile.nicheng or '' # 6. 罚款数据(如果存在) - fadan_obj = Fadan.objects.filter( + fadan_obj = Fadan.query.filter( guanliandingdan_id=dingdan_id, beichufa_id=dashou_yonghuid ).first() @@ -2253,7 +2253,7 @@ class ShangjiaDingdanXiangqingView(APIView): fenhong_lilv = 0.0 try: # 查找商家作为分红者的最新记录,取应得利率 - last_fenhong = FadanFenhong.objects.filter( + last_fenhong = FadanFenhong.query.filter( fenhongzhe_id=yonghuid, shenfen=1 # 1=商家 ).order_by('-create_time').first() @@ -2266,7 +2266,7 @@ class ShangjiaDingdanXiangqingView(APIView): xin_dingdan_id = '' try: # 查找以当前订单作为“上一个订单ID”的新订单扩展记录 - new_shangjia_order = DingdanShangjia.objects.filter( + new_shangjia_order = DingdanShangjia.query.filter( shangyige_dingdan_id=dingdan_id, shangjia_id=yonghuid ).select_related('dingdan').first() @@ -2390,7 +2390,7 @@ class ShangjiaJiesuanView(APIView): order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id) # 验证订单是否属于当前商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( dingdan=order, shangjia_id=yonghuid ).first() @@ -2425,7 +2425,7 @@ class ShangjiaJiesuanView(APIView): # ========== 本地订单正常结算给打手 ========== # 查询打手信息 try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou = dashou_user.dashou_profile except (UserMain.DoesNotExist, UserDashou.DoesNotExist): return Response({ @@ -2435,7 +2435,7 @@ class ShangjiaJiesuanView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 原子更新打手数据(使用 F 表达式) - UserDashou.objects.filter(id=dashou.id).update( + UserDashou.query.filter(id=dashou.id).update( chengjiaozongliang=F('chengjiaozongliang') + 1, yue=F('yue') + dashou_fencheng, zonge=F('zonge') + dashou_fencheng, @@ -2444,7 +2444,7 @@ class ShangjiaJiesuanView(APIView): ) # 更新商家成交单量 - UserShangjia.objects.filter(id=shangjia.id).update( + UserShangjia.query.filter(id=shangjia.id).update( chengjiao=F('chengjiao') + 1, ) @@ -2454,18 +2454,18 @@ class ShangjiaJiesuanView(APIView): # 处理评分(所有情况都处理) if pingfen > 0: - pingfen_obj, created = Pingfen.objects.get_or_create( + pingfen_obj, created = Pingfen.query.get_or_create( dashou=jiedan_dashou_id, defaults={'zongfen': pingfen, 'cishu': 1} ) if not created: - Pingfen.objects.filter(id=pingfen_obj.id).update( + Pingfen.query.filter(id=pingfen_obj.id).update( zongfen=F('zongfen') + pingfen, cishu=F('cishu') + 1, ) # 更新商家订单扩展表的评价 - DingdanShangjia.objects.filter(id=shangjia_kuozhan.id).update( + DingdanShangjia.query.filter(id=shangjia_kuozhan.id).update( shangjia_pingjia=liuyan, ) @@ -2573,7 +2573,7 @@ class ShangjiaChexiaoView(APIView): # 5. 使用事务确保数据一致性 with transaction.atomic(): # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + dingdan = Dingdan.query.filter(dingdan_id=dingdan_id).first() if not dingdan: return Response({ 'code': 5, @@ -2590,7 +2590,7 @@ class ShangjiaChexiaoView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证订单所属商家(使用反向查询) - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( dingdan__dingdan_id=dingdan_id, shangjia_id=current_user.yonghuid ).first() @@ -2603,7 +2603,7 @@ class ShangjiaChexiaoView(APIView): }, status=status.HTTP_403_FORBIDDEN) # 8. 创建退款记录(注意:申请状态直接设为1-成功) - Tuikuanjilu.objects.create( + Tuikuanjilu.query.create( dingdan_id=dingdan_id, qingqiuid=current_user.yonghuid, liyou=chexiao_liyou, @@ -2617,7 +2617,7 @@ class ShangjiaChexiaoView(APIView): ) # 9. 更新商家扩展表数据(使用F表达式原子操作) - UserShangjia.objects.filter(id=shangjia.id).update( + UserShangjia.query.filter(id=shangjia.id).update( yue=F('yue') + dingdan.jine, tuikuan=F('tuikuan') + 1, jinridingdan=F('jinridingdan') - 1, @@ -2709,7 +2709,7 @@ class ShangjiaTuikuanShenqingView(APIView): # 5. 使用事务确保数据一致性 with transaction.atomic(): # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + dingdan = Dingdan.query.filter(dingdan_id=dingdan_id).first() if not dingdan: return Response({ 'code': 5, @@ -2726,7 +2726,7 @@ class ShangjiaTuikuanShenqingView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( dingdan__dingdan_id=dingdan_id, shangjia_id=current_user.yonghuid ).first() @@ -2739,7 +2739,7 @@ class ShangjiaTuikuanShenqingView(APIView): }, status=status.HTTP_403_FORBIDDEN) # ========== 8. 处理退款记录(修改点:先查询是否存在) ========== - refund_record, created = Tuikuanjilu.objects.get_or_create( + refund_record, created = Tuikuanjilu.query.get_or_create( dingdan_id=dingdan_id, defaults={ 'dashouid': dingdan.jiedan_dashou_id, @@ -2767,7 +2767,7 @@ class ShangjiaTuikuanShenqingView(APIView): # ====== 原有功能:更新打手状态 ====== if dingdan.jiedan_dashou_id: try: - dashou_main = UserMain.objects.filter( + dashou_main = UserMain.query.filter( yonghuid=dingdan.jiedan_dashou_id ).first() if dashou_main and hasattr(dashou_main, 'dashou_profile'): @@ -2850,7 +2850,7 @@ class ShangjiaChufaShenqingView(APIView): with transaction.atomic(): # 验证打手身份 try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou = dashou_user.dashou_profile except UserMain.DoesNotExist: return Response({ @@ -2866,7 +2866,7 @@ class ShangjiaChufaShenqingView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 查询订单主表 - dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first() + dingdan = Dingdan.query.filter(dingdan_id=dingdan_id).first() if not dingdan: return Response({ 'code': 6, @@ -2883,7 +2883,7 @@ class ShangjiaChufaShenqingView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 验证订单所属商家 - shangjia_kuozhan = DingdanShangjia.objects.filter( + shangjia_kuozhan = DingdanShangjia.query.filter( dingdan__dingdan_id=dingdan_id, shangjia_id=current_user.yonghuid ).first() @@ -2904,7 +2904,7 @@ class ShangjiaChufaShenqingView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 创建处罚记录 - chufajilu = Chufajilu.objects.create( + chufajilu = Chufajilu.query.create( dashouid=dashou_id, qingqiuid=current_user.yonghuid, cfliyou=chufa_liyou, @@ -2927,7 +2927,7 @@ class ShangjiaChufaShenqingView(APIView): if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): for tupian_url in chufa_tupian_urls: if tupian_url and tupian_url.strip(): - Chufatupian.objects.create( + Chufatupian.query.create( dingdan_id=dingdan_id, yonghuid=current_user.yonghuid, tupian=tupian_url @@ -2988,13 +2988,13 @@ class DashouDingdanHuoquView(APIView): # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突 if biaoqian_id and int(biaoqian_id) > 0: - biaoqian_order_ids = DingdanBiaoqian.objects.filter( + biaoqian_order_ids = DingdanBiaoqian.query.filter( chenghao_id=int(biaoqian_id) ).values_list('dingdan_id', flat=True) base_query &= Q(dingdan_id__in=list(biaoqian_order_ids)) # 4. 构建基础查询集 - base_qs = Dingdan.objects.filter(base_query).order_by('-create_time').distinct() + base_qs = Dingdan.query.filter(base_query).order_by('-create_time').distinct() # 5. 获取总记录数 total = base_qs.count() @@ -3007,7 +3007,7 @@ class DashouDingdanHuoquView(APIView): page_qs = base_qs[offset:offset + limit] # 8. 使用子查询获取商家昵称 - shangjia_sub = DingdanShangjia.objects.filter( + shangjia_sub = DingdanShangjia.query.filter( dingdan__dingdan_id=OuterRef('dingdan_id') ).values('sjnicheng')[:1] @@ -3028,7 +3028,7 @@ class DashouDingdanHuoquView(APIView): zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')} zhiding_info = {} if zhiding_ids: - dashou_query = UserMain.objects.filter( + dashou_query = UserMain.query.filter( yonghuid__in=zhiding_ids ).select_related('dashou_profile').annotate( nicheng=F('dashou_profile__nicheng') @@ -3048,7 +3048,7 @@ class DashouDingdanHuoquView(APIView): xuqiu_tags_map = {} if order_ids: try: - dbq_qs = DingdanBiaoqian.objects.filter( + dbq_qs = DingdanBiaoqian.query.filter( dingdan_id__in=order_ids ).select_related('chenghao') for dbq in dbq_qs: @@ -3070,7 +3070,7 @@ class DashouDingdanHuoquView(APIView): shangjia_id_map = {} if shangjia_order_ids: # 使用跨表查询 dingdan__dingdan_id__in 匹配字符串订单ID - sj_exts = DingdanShangjia.objects.filter( + sj_exts = DingdanShangjia.query.filter( dingdan__dingdan_id__in=shangjia_order_ids ).values('dingdan_id', 'shangjia_id') for sj in sj_exts: @@ -3081,14 +3081,14 @@ class DashouDingdanHuoquView(APIView): # 将 yonghuid 字符串转换为 UserMain 的主键 id user_id_map = {} if yonghuid_set: - users = UserMain.objects.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') + users = UserMain.query.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid') for u in users: user_id_map[u['yonghuid']] = u['id'] user_main_ids = list(user_id_map.values()) user_tags_map = {} if user_main_ids: - uc_qs = YonghuChenghao.objects.filter( + uc_qs = YonghuChenghao.query.filter( yonghu_id__in=user_main_ids ).select_related('chenghao') for uc in uc_qs: @@ -3207,7 +3207,7 @@ class ShangpinLeixingHuoquView(APIView): def post(self, request): # 1. 查询商品类型表 # 假设你的商品类型模型字段为:id, jieshao, tupian_url - leixing_list = ShangpinLeixing.objects.all().values( + leixing_list = ShangpinLeixing.query.all().values( 'id', # 商品类型ID 'jieshao', # 类型介绍 'tupian_url' # 类型图片URL(相对URL) @@ -3346,13 +3346,13 @@ def send_group_message( def check_user_permission(identity_type, uid, order): """验证用户身份、账号状态、以及是否属于该订单""" try: - user = UserMain.objects.get(yonghuid=uid) + user = UserMain.query.get(yonghuid=uid) except UserMain.DoesNotExist: return False, '用户不存在' if identity_type == 'dashou': try: - dashou = UserDashou.objects.get(user=user) + dashou = UserDashou.query.get(user=user) if dashou.zhanghaozhuangtai != 1 : return False, '打手账号已被封禁或状态异常' except UserDashou.DoesNotExist: @@ -3362,7 +3362,7 @@ def check_user_permission(identity_type, uid, order): elif identity_type == 'shangjia': try: - shop = UserShangjia.objects.get(user=user) + shop = UserShangjia.query.get(user=user) if shop.zhuangtai != 1: return False, '商家账号已被禁用' except UserShangjia.DoesNotExist: @@ -3375,7 +3375,7 @@ def check_user_permission(identity_type, uid, order): elif identity_type == 'boss': try: - UserBoss.objects.get(user=user) # 老板暂无封禁字段,仅需存在 + UserBoss.query.get(user=user) # 老板暂无封禁字段,仅需存在 except UserBoss.DoesNotExist: return False, '该用户不是老板' try: @@ -3424,7 +3424,7 @@ class DaiLiQunLiaoXiaoXiView(APIView): # ===== 3. 查询订单 ===== try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) + order = Dingdan.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id) except Dingdan.DoesNotExist: logger.warning(f"[群聊代理] 订单 {order_id} 不存在") return Response({'code': 404, 'msg': '订单不存在'}) @@ -3491,7 +3491,7 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView): return Response({'code': 400, 'msg': '缺少参数'}) try: - lianjie = ShangjiaLianjie.objects.get(lianjie_token=token) + lianjie = ShangjiaLianjie.query.get(lianjie_token=token) except ShangjiaLianjie.DoesNotExist: return Response({'code': 404, 'msg': '链接不存在或已失效'}) @@ -3499,7 +3499,7 @@ class ShangjiaLianjieLiuYanZhuanFaView(APIView): return Response({'code': 400, 'msg': '链接缺少订单ID'}) try: - order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id) + order = Dingdan.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 500, 'msg': '订单数据异常'}) @@ -3650,7 +3650,7 @@ class QiangdanView(APIView): def _validate_huiyuan_requirement(self, order, dashou_yonghuid): try: - huiyuan_record = Huiyuangoumai.objects.get( + huiyuan_record = Huiyuangoumai.query.get( huiyuan_id=order.huiyuan_id, yonghu_id=dashou_yonghuid ) @@ -3745,7 +3745,7 @@ class DashouDingdanHuoquView1(APIView): query &= keyword_query # 5. 执行查询,按创建时间倒序 - dingdan_query = Dingdan.objects.filter(query).order_by('-create_time') + dingdan_query = Dingdan.query.filter(query).order_by('-create_time') # 只选择需要的字段,提高性能 dingdan_query = dingdan_query.only( @@ -3826,7 +3826,7 @@ class DashouCOSZhengshuView(APIView): # 3. 业务验证逻辑(根据用途分别验证) if yongtu == 'chufa': try: - chufa_record = Chufajilu.objects.filter( + chufa_record = Chufajilu.query.filter( dingdan_id=dingdan_id, dashouid=request.user.yonghuid, sqzhuangtai__in=[0, 3] @@ -3849,7 +3849,7 @@ class DashouCOSZhengshuView(APIView): else: # 默认为订单相关,需要验证订单且打手必须接单 try: - dingdan = Dingdan.objects.get( + dingdan = Dingdan.query.get( dingdan_id=dingdan_id, jiedan_dashou_id=request.user.yonghuid, zhuangtai__in=[2, 8] @@ -3985,7 +3985,7 @@ class ShangjiaCOSZhengshuView(APIView): # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 try: # 获取订单 - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家 try: @@ -4005,7 +4005,7 @@ class ShangjiaCOSZhengshuView(APIView): # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 try: - existing_chufa = Chufajilu.objects.filter( + existing_chufa = Chufajilu.query.filter( dingdan_id=dingdan_id, qingqiuid=shangjia_id, sqzhuangtai__in=[0, 1] # 审核中或已处罚状态 @@ -4191,7 +4191,7 @@ class DashouTijiaoView(APIView): # 4. 验证订单状态和权限 try: - dingdan = Dingdan.objects.get( + dingdan = Dingdan.query.get( dingdan_id=dingdan_id, jiedan_dashou_id=yonghuid, zhuangtai=2 # 必须是进行中的订单 @@ -4206,7 +4206,7 @@ class DashouTijiaoView(APIView): # 5. 存储图片到Dashoutupian表 for tupian_url in jiaofu_tupian_urls: # 创建打手提交图片记录 - Dashoutupian.objects.create( + Dashoutupian.query.create( dingdan_id=dingdan_id, dashou=yonghuid, tupian=tupian_url, @@ -4283,7 +4283,7 @@ class DashouDingdanXiangqingView(APIView): # 3. 查询订单主表 try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({ 'code': 3, @@ -4316,7 +4316,7 @@ class DashouDingdanXiangqingView(APIView): } # 6. 查询打手提交的图片 - dashou_tupian_list = Dashoutupian.objects.filter( + dashou_tupian_list = Dashoutupian.query.filter( dingdan_id=dingdan_id, dashou=yonghuid ).order_by('create_time') @@ -4451,7 +4451,7 @@ class AdGetDingdanXiangQing(APIView): try: # 查询订单主表 - 使用select_related预加载关联表 - dingdan_obj = Dingdan.objects.select_related( + dingdan_obj = Dingdan.query.select_related( 'pingtai_kuozhan', 'shangjia_kuozhan' ).get(dingdan_id=dingdan_id) @@ -4534,7 +4534,7 @@ class AdGetDingdanXiangQing(APIView): # 9. 查询打手提交的图片 try: # 查询该订单的所有打手提交图片 - dashou_tupian_list = Dashoutupian.objects.filter( + dashou_tupian_list = Dashoutupian.query.filter( dingdan_id=dingdan_id ).values_list('tupian', flat=True) @@ -4549,7 +4549,7 @@ class AdGetDingdanXiangQing(APIView): try: # 导入退款记录模型 # 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的) - tuikuan_record = Tuikuanjilu.objects.filter( + tuikuan_record = Tuikuanjilu.query.filter( dingdan_id=dingdan_id ).order_by('-create_time').first() # 按创建时间倒序,取最新的 @@ -4656,7 +4656,7 @@ class AdGengHuanDaShou(APIView): # 6. 查询订单主表 try: - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -4672,7 +4672,7 @@ class AdGengHuanDaShou(APIView): # 8. 验证新打手ID是否存在,并且是打手身份 try: - new_dashou_user = UserMain.objects.get( + new_dashou_user = UserMain.query.get( yonghuid=new_dashou_id, #user_type='dashou' ) @@ -4712,7 +4712,7 @@ class AdGengHuanDaShou(APIView): # 查询会员购买记录 try: - huiyuan_goumai = Huiyuangoumai.objects.get( + huiyuan_goumai = Huiyuangoumai.query.get( huiyuan_id=huiyuan_id, yonghu_id=new_dashou_id # 注意:yonghu_id存储的是用户ID(yonghuid) ) @@ -4767,7 +4767,7 @@ class AdGengHuanDaShou(APIView): if old_dashou_id: try: # 查询原打手主表 - old_dashou_user = UserMain.objects.get( + old_dashou_user = UserMain.query.get( yonghuid=old_dashou_id, #user_type='dashou' ) @@ -4875,7 +4875,7 @@ class AdQiangZhiJieDan(APIView): # 6. 查询订单主表 try: - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -4911,7 +4911,7 @@ class AdQiangZhiJieDan(APIView): if jiedan_dashou_id: try: # 查询打手用户主表 - dashou_user = UserMain.objects.get( + dashou_user = UserMain.query.get( yonghuid=jiedan_dashou_id, #user_type='dashou' ) @@ -4954,7 +4954,7 @@ class AdQiangZhiJieDan(APIView): if shangjia_id: # 查询商家用户主表 - shangjia_user = UserMain.objects.get( + shangjia_user = UserMain.query.get( yonghuid=shangjia_id, #user_type='shop' ) @@ -5058,7 +5058,7 @@ class AdJuJueTuiKuan(APIView): # 6. 查询订单主表 try: - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -5087,7 +5087,7 @@ class AdJuJueTuiKuan(APIView): if jiedan_dashou_id: try: # 查询打手用户主表 - dashou_user = UserMain.objects.get( + dashou_user = UserMain.query.get( yonghuid=jiedan_dashou_id, #user_type='dashou' ) @@ -5130,7 +5130,7 @@ class AdJuJueTuiKuan(APIView): if shangjia_id: # 查询商家用户主表 - shangjia_user = UserMain.objects.get( + shangjia_user = UserMain.query.get( yonghuid=shangjia_id, #user_type='shop' ) @@ -5157,7 +5157,7 @@ class AdJuJueTuiKuan(APIView): # 9.4 更新退款记录表 try: # 查询退款记录表 - tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + tuikuan_jilu = Tuikuanjilu.query.get(dingdan_id=dingdan_id) # ✅ 确认字段:sqzhuangtai 申请状态,改为2(驳回) tuikuan_jilu.sqzhuangtai = 2 @@ -5266,7 +5266,7 @@ class AdTongYiTuiKuanShangJia(APIView): # 7. 查询订单主表 try: - dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan_obj = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -5318,7 +5318,7 @@ class AdTongYiTuiKuanShangJia(APIView): if jiedan_dashou_id: try: # 查询打手用户主表 - dashou_user = UserMain.objects.get( + dashou_user = UserMain.query.get( yonghuid=jiedan_dashou_id, #user_type='dashou' ) @@ -5343,7 +5343,7 @@ class AdTongYiTuiKuanShangJia(APIView): if shangjia_id: try: # 查询商家用户主表 - shangjia_user = UserMain.objects.get( + shangjia_user = UserMain.query.get( yonghuid=shangjia_id, #user_type='shop' ) @@ -5370,7 +5370,7 @@ class AdTongYiTuiKuanShangJia(APIView): # 13.4 更新退款记录表 try: # 查询退款记录表 - tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + tuikuan_jilu = Tuikuanjilu.query.get(dingdan_id=dingdan_id) # ✅ 确认字段:sqzhuangtai 申请状态,改为1(成功) tuikuan_jilu.sqzhuangtai = 1 @@ -5528,7 +5528,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 14. 更新打手扩展表 if jiedan_dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.tuikuanliang += 1 @@ -5539,7 +5539,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 15. 更新老板扩展表 if laoban_id: try: - laoban_user = UserMain.objects.get(yonghuid=laoban_id) + laoban_user = UserMain.query.get(yonghuid=laoban_id) laoban_profile = laoban_user.boss_profile laoban_profile.alltui += 1 laoban_profile.zonge -= jine @@ -5555,7 +5555,7 @@ class AdTongYiTuiKuanPingTai(APIView): update_daily_payout(jine) - szjilu_obj = Szjilu.objects.get(id=1) + szjilu_obj = Szjilu.query.get(id=1) szjilu_obj.zongsy -= jine szjilu_obj.zongzc += jine szjilu_obj.jrzc += jine @@ -5567,7 +5567,7 @@ class AdTongYiTuiKuanPingTai(APIView): # 17. 更新退款记录表 try: - tuikuan_jilu, created = Tuikuanjilu.objects.get_or_create( + tuikuan_jilu, created = Tuikuanjilu.query.get_or_create( dingdan_id=dingdan_id, defaults={ 'dashouid': jiedan_dashou_id if jiedan_dashou_id else '', @@ -5742,7 +5742,7 @@ class DashouHuoquLeixingView(APIView): # 🔴 查询所有正常状态的商品类型 # 条件:shenhezhuangtai=1(正常状态) # 排序:先按 paixu 降序(数值大的在前),再按 id 升序 - leixing_queryset = ShangpinLeixing.objects.filter( + leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=1 # 🔴 只取正常状态,排除审核专用 ).order_by('-paixu', 'id') @@ -5835,7 +5835,7 @@ class AdJuJueJieSuan(APIView): # 5. 查询订单 try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -5864,7 +5864,7 @@ class AdJuJueJieSuan(APIView): # 8. 更新打手扩展表 try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为正常 dashou_profile.tuikuanliang += 1 # 退款订单总量+1 @@ -5921,7 +5921,7 @@ class AdZhuanYiDaTing(APIView): ) try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -5946,7 +5946,7 @@ class AdZhuanYiDaTing(APIView): # 如果有接单打手,更新其扩展表 if jiedan_dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 dashou_profile.tuikuanliang += 1 @@ -6003,7 +6003,7 @@ class AdQuXiaoZhiDing(APIView): ) try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response( {'code': 404, 'message': '订单不存在', 'data': None}, @@ -6089,7 +6089,7 @@ class DashouXiugaiView(APIView): # ---------- 订单查询及权限验证 ---------- try: # 订单必须属于该打手,且状态为4(退款中)或8(结算中) - dingdan = Dingdan.objects.get( + dingdan = Dingdan.query.get( dingdan_id=dingdan_id, jiedan_dashou_id=yonghuid, zhuangtai__in=[4, 8] # 只允许修改退款中或结算中的订单 @@ -6104,7 +6104,7 @@ class DashouXiugaiView(APIView): # 1. 处理删除的图片:从 Dashoutupian 表中删除记录 if deleted_images: # 根据订单ID、打手ID和图片URL删除记录 - delete_count, _ = Dashoutupian.objects.filter( + delete_count, _ = Dashoutupian.query.filter( dingdan_id=dingdan_id, dashou=yonghuid, tupian__in=deleted_images @@ -6184,7 +6184,7 @@ class ZxsjghdsView(APIView): # ========== 3. 查询订单并校验权限 ========== try: # 关联查询商家扩展表,确保当前商家是该订单的发布者 - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + shangjia_kuozhan = DingdanShangjia.query.select_related('dingdan').get( dingdan__dingdan_id=dingdan_id, shangjia_id=yonghuid ) @@ -6224,7 +6224,7 @@ class ZxsjghdsView(APIView): # 6.2 释放打手 if dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 dashou_profile.zhuangtai = 1 # 空闲 @@ -6233,7 +6233,7 @@ class ZxsjghdsView(APIView): logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新") # 6.3 更新退款记录 - Tuikuanjilu.objects.update_or_create( + Tuikuanjilu.query.update_or_create( dingdan_id=dingdan_id, defaults={ 'dashouid': dashou_id or '', @@ -6277,11 +6277,11 @@ class ZxsjghdsView(APIView): new_dingdan_id = generate_dingdan_id() # 确保唯一 - while Dingdan.objects.filter(dingdan_id=new_dingdan_id).exists(): + while Dingdan.query.filter(dingdan_id=new_dingdan_id).exists(): new_dingdan_id = generate_dingdan_id() # 7.2 创建新订单(复制数据,状态为 1) - new_order = Dingdan.objects.create( + new_order = Dingdan.query.create( dingdan_id=new_dingdan_id, zhuangtai=1, # 已下单 fadan_pingtai=fadan_pingtai, @@ -6300,7 +6300,7 @@ class ZxsjghdsView(APIView): ) # 7.3 创建商家扩展表(关联新订单) - DingdanShangjia.objects.create( + DingdanShangjia.query.create( dingdan=new_order, shangjia_id=yonghuid, sjnicheng=shangjia_kuozhan.sjnicheng or '', @@ -6337,7 +6337,7 @@ class ZxsjghdsView(APIView): def _get_game_type_name(self, leixing_id): """根据类型ID获取游戏类型名称""" try: - gt = ShangpinLeixing.objects.filter(id=leixing_id).first() + gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception: return '游戏订单' @@ -6393,7 +6393,7 @@ class ShangjiaFakuanApplyView(APIView): # ========== 3. 获取订单,校验归属商家 ========== try: # 关联查询商家扩展表,确保订单属于当前商家 - shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get( + shangjia_kuozhan = DingdanShangjia.query.select_related('dingdan').get( dingdan__dingdan_id=dingdan_id, shangjia_id=shangjia_id ) @@ -6407,12 +6407,12 @@ class ShangjiaFakuanApplyView(APIView): return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) # ========== 5. 重复罚款检查 ========== - if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + if Fadan.query.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) # ========== 6. 本地创建罚单 ========== with transaction.atomic(): - Fadan.objects.create( + Fadan.query.create( beichufa_id=dashou_id, shenqing_chufa=shangjia_id, # 申请人为当前商家 shenfen=1, # 被打者身份:1=打手 @@ -6451,7 +6451,7 @@ class ShangjiaFakuaiXiugaiView(APIView): # 验证订单属于该商家 try: - dingdan = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.shangjia_id != user.yonghuid: @@ -6468,7 +6468,7 @@ class ShangjiaFakuaiXiugaiView(APIView): if not fadan_id: return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) try: - fadan = Fadan.objects.get(id=fadan_id) + fadan = Fadan.query.get(id=fadan_id) except Fadan.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) if fadan.guanliandingdan_id != dingdan_id: diff --git a/products/models.py b/products/models.py index c91f3ab..d986c26 100644 --- a/products/models.py +++ b/products/models.py @@ -1,8 +1,9 @@ from django.db import models +from gvsdsdk.model_base import QModel from django.utils import timezone -class Shangpin(models.Model): +class Shangpin(QModel): """商品表 - 100-1000条数据""" id = models.AutoField(primary_key=True, verbose_name='商品ID') biaoqian = models.CharField(max_length=45,null=True, blank=True, verbose_name='商品标题') # 15汉字 @@ -67,7 +68,7 @@ class Shangpin(models.Model): -class ShangpinLeixing(models.Model): +class ShangpinLeixing(QModel): """商品类型表 - <10条数据""" id = models.AutoField(primary_key=True, verbose_name='类型ID') yaoqiuleixing = models.PositiveIntegerField(null=True, blank=True, verbose_name='抢单要求类型') @@ -94,7 +95,7 @@ class ShangpinLeixing(models.Model): return f"{self.jieshao}" -class ShangpinZhuanqu(models.Model): +class ShangpinZhuanqu(QModel): """商品专区表 - <50条数据""" id = models.AutoField(primary_key=True, verbose_name='专区ID') mingzi = models.CharField(max_length=45, null=True, blank=True, verbose_name='专区名称') # 15汉字 @@ -131,7 +132,7 @@ class ShangpinZhuanqu(models.Model): return f"{self.mingzi}" -class Huiyuan(models.Model): +class Huiyuan(QModel): huiyuan_id = models.CharField(max_length=6, unique=True, verbose_name='会员ID') jieshao = models.CharField(max_length=30, verbose_name='会员介绍') jtjieshao = models.TextField(null=True, blank=True, verbose_name='会员规则介绍') @@ -159,7 +160,7 @@ class Huiyuan(models.Model): return f"会员{self.huiyuan_id}:{self.jieshao}" -class Huiyuangoumai(models.Model): +class Huiyuangoumai(QModel): id = models.AutoField(primary_key=True, verbose_name='购买记录ID') huiyuan_id = models.CharField(max_length=6, db_index=True, verbose_name='会员ID') yonghu_id = models.CharField(max_length=32, db_index=True, verbose_name='用户ID') @@ -203,7 +204,7 @@ class Huiyuangoumai(models.Model): return False -class Czjilu(models.Model): +class Czjilu(QModel): dingdan_id = models.CharField(max_length=32, unique=True, db_index=True, verbose_name='订单ID') huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID') zhuangtai = models.IntegerField(null=True, blank=True, verbose_name='订单状态') @@ -228,7 +229,7 @@ class Czjilu(models.Model): -class Gsfenhong(models.Model): +class Gsfenhong(QModel): dingdan_id = models.CharField(max_length=32, unique=True, verbose_name='订单ID') guanshi = models.CharField(max_length=7, db_index=True, verbose_name='管事ID') dashouid = models.CharField(max_length=7, db_index=True,verbose_name='打手ID') @@ -267,7 +268,7 @@ class Gsfenhong(models.Model): -class DuociFenhong(models.Model): +class DuociFenhong(QModel): """ 多次分红配置表 用于配置不同次数下管事和组长的分红金额 @@ -300,7 +301,7 @@ class DuociFenhong(models.Model): -class ShangpinMeiriTongji(models.Model): +class ShangpinMeiriTongji(QModel): """ 商品每日统计表 按商品 + 日期维度记录点单、成交、退款及收益数据 diff --git a/products/views.py b/products/views.py index 942e2ee..b6c0d0a 100644 --- a/products/views.py +++ b/products/views.py @@ -17,6 +17,7 @@ import defusedxml.ElementTree as ET from django.conf import settings from django.db import transaction, connection from django.db.models import Q +from gvsdsdk.fluent import db, func, FQ from django.shortcuts import render, get_object_or_404 from django.http import HttpResponse from django.utils import timezone @@ -78,12 +79,12 @@ class ShangpinHuoquView(APIView): # 1. 查询商品类型(只查询正常状态的类型,并排序) # 正常运营:只显示 shenhezhuangtai=2 的商品类型 - leixing_queryset = ShangpinLeixing.objects.filter( + leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=1 # 只取正常状态 ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 # 注意:审核期间使用下面这行代码(先注释掉) - '''leixing_queryset = ShangpinLeixing.objects.filter( + '''leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=2 # 审核期间:只取审核专用状态 ).order_by('-paixu', 'id')''' @@ -101,12 +102,12 @@ class ShangpinHuoquView(APIView): visible_leixing_ids = [leixing['id'] for leixing in shangpinleixing_data] if visible_leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.objects.filter( + zhuanqu_queryset = ShangpinZhuanqu.query.filter( leixing_id__in=visible_leixing_ids, shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的专区 ).order_by('-paixu', 'id') # 先按paixu降序,再按id升序 else: - zhuanqu_queryset = ShangpinZhuanqu.objects.none() + zhuanqu_queryset = ShangpinZhuanqu.query.none() shangpinzhuanqu_data = [] for zhuanqu in zhuanqu_queryset: @@ -123,7 +124,7 @@ class ShangpinHuoquView(APIView): if visible_zhuanqu_ids: # 注意:这里使用values()只获取指定字段,提高查询效率 - shangpin_queryset = Shangpin.objects.filter( + shangpin_queryset = Shangpin.query.filter( zhuanqu_id__in=visible_zhuanqu_ids, shenhezhuangtai=1 # 🔴【添加】只筛选审核状态为2的商品 ).values( @@ -186,7 +187,7 @@ class ShangpinXiangqingHuoquView(APIView): }, status=400) # 直接查询,数据量小,性能没问题 - shangpin = Shangpin.objects.filter(id=shangpin_id).first() + shangpin = Shangpin.query.filter(id=shangpin_id).first() if not shangpin: return Response({ @@ -256,7 +257,7 @@ class DashouHuiyuanList(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 3. 查询所有会员数据(ORM查询,确保性能) - huiyuan_queryset = Huiyuan.objects.all().only( + huiyuan_queryset = Huiyuan.query.all().only( 'huiyuan_id', # 会员ID 'jieshao', # 会员名字(注意:jieshao字段实际存储的是会员名字) 'jtjieshao', # 会员具体介绍 @@ -349,7 +350,7 @@ class YajinGoumai(APIView): dingdanid = f"CzYJ{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" # 4. 创建充值记录订单 - chongzhi_jilu = Czjilu.objects.create( + chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.yonghuid, @@ -576,7 +577,7 @@ class YajinHuitiao(View): # 4. 查询订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) + order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: logger.error(f"订单不存在: {out_trade_no}") @@ -605,7 +606,7 @@ class YajinHuitiao(View): # 8. 更新打手押金 try: # 通过用户ID查找打手(高效查询) - dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) + dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) # 增加押金(使用Decimal确保精度) dashou.yajin += Decimal(str(order_amount_yuan)) @@ -619,10 +620,10 @@ class YajinHuitiao(View): guanshi_id = dashou.yaoqingren if not guanshi_id: logger.info(f"押金分红跳过: 打手{order.yonghuid}无邀请管事") - elif Gsfenhong.objects.filter(dingdan_id=order.dingdan_id).exists(): + elif Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists(): logger.info(f"押金分红已处理: 订单{order.dingdan_id}") else: - lilu_obj = Lilubiao.objects.filter(fadanpingtai='12').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='12').first() rate = lilu_obj.lilu if lilu_obj and lilu_obj.lilu is not None else Decimal('0') if rate <= 0: logger.info(f"押金分红跳过: 费率未配置或为0") @@ -639,7 +640,7 @@ class YajinHuitiao(View): guanshi.save(update_fields=['yue', 'chongzhifenrun']) user_main = dashou.user - Gsfenhong.objects.create( + Gsfenhong.query.create( dingdan_id=order.dingdan_id, guanshi=guanshi_id, dashouid=order.yonghuid, @@ -684,7 +685,7 @@ class YajinHuitiao(View): # 查询或创建ID=1的收支记录 update_daily_income(jine_decimal) - szjilu, created = Szjilu.objects.get_or_create( + szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ 'zongsy': Decimal('0.00'), @@ -795,7 +796,7 @@ class YajinShibai(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -853,7 +854,7 @@ class YajinLunxun(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -945,7 +946,7 @@ class JifenBuchong(APIView): # 5. 创建积分充值记录订单(固定5元) jine = 5.00 # 固定5元 - chongzhi_jilu = Czjilu.objects.create( + chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.yonghuid, @@ -1170,7 +1171,7 @@ class JifenHuitiao(View): # 4. 查询订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) + order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到积分订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: logger.error(f"积分订单不存在: {out_trade_no}") @@ -1199,7 +1200,7 @@ class JifenHuitiao(View): update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.objects.get_or_create( + szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ 'zongsy': Decimal('0.00'), @@ -1233,7 +1234,7 @@ class JifenHuitiao(View): # 8. 更新打手积分 try: # 通过用户ID查找打手 - dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) + dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) # 检查积分是否小于10 if dashou.jifen < 10: @@ -1339,7 +1340,7 @@ class JifenShibai(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -1404,7 +1405,7 @@ class JifenLunxun(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -1497,7 +1498,7 @@ class HuiyuanGoumai(APIView): # 4. 查询会员信息 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuanid) + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuanid) except Huiyuan.DoesNotExist: return Response({ 'code': 400, @@ -1516,7 +1517,7 @@ class HuiyuanGoumai(APIView): # 组合订单说明:会员介绍 + 配置描述 shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}" - chongzhi_jilu = Czjilu.objects.create( + chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.yonghuid, @@ -1741,7 +1742,7 @@ class HuiyuanHuitiao(View): # 查询订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) + order = Czjilu.query.get(dingdan_id=out_trade_no) except Czjilu.DoesNotExist: logger.error(f"会员订单不存在: {out_trade_no}") return self.wechat_response('FAIL', '订单不存在') @@ -1763,7 +1764,7 @@ class HuiyuanHuitiao(View): # 查询会员信息 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=order.huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) except Huiyuan.DoesNotExist: logger.error(f"会员信息不存在: {order.huiyuan_id}") return self.wechat_response('FAIL', '会员信息不存在') @@ -1774,7 +1775,7 @@ class HuiyuanHuitiao(View): update_daily_income(jine_decimal) - szjilu, _ = Szjilu.objects.get_or_create( + szjilu, _ = Szjilu.query.get_or_create( id=1, defaults={'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'), 'zongzc': Decimal('0.00'), 'jrzc': Decimal('0.00'), 'jrls': Decimal('0.00')} @@ -1823,7 +1824,7 @@ class HuiyuanHuitiao(View): def process_huiyuan_goumai(self, yonghuid, huiyuan_id, huiyuan): """处理会员购买记录(与原代码一致)""" try: - existing_record = Huiyuangoumai.objects.filter( + existing_record = Huiyuangoumai.query.filter( yonghu_id=yonghuid, huiyuan_id=huiyuan_id ).first() @@ -1837,14 +1838,14 @@ class HuiyuanHuitiao(View): logger.info(f"会员续费: 用户{yonghuid}, 会员{huiyuan_id}") return existing_record, False, False else: - other_huiyuan_count = Huiyuangoumai.objects.filter( + other_huiyuan_count = Huiyuangoumai.query.filter( yonghu_id=yonghuid ).exclude(huiyuan_id=huiyuan_id).count() if other_huiyuan_count == 0: is_first_buy_any = True logger.info(f"用户{yonghuid}首次购买任何会员,将获得10积分") - goumai_record = Huiyuangoumai.objects.create( + goumai_record = Huiyuangoumai.query.create( yonghu_id=yonghuid, huiyuan_id=huiyuan_id, jieshao=huiyuan.jieshao, @@ -1863,7 +1864,7 @@ class HuiyuanHuitiao(View): def add_first_buy_jifen(self, yonghuid): """首次购买积分奖励(与原代码一致)""" try: - dashou = UserDashou.objects.get(user__yonghuid=yonghuid) + dashou = UserDashou.query.get(user__yonghuid=yonghuid) dashou.jifen += 10 dashou.save(update_fields=['jifen']) logger.info(f"用户{yonghuid}首次购买会员,积分+10,当前积分: {dashou.jifen}") @@ -1879,7 +1880,7 @@ class HuiyuanHuitiao(View): """ try: # 获取打手信息 - dashou = UserDashou.objects.get(user__yonghuid=order.yonghuid) + dashou = UserDashou.query.get(user__yonghuid=order.yonghuid) guanshi_id = dashou.yaoqingren if not guanshi_id: logger.info(f"打手{order.yonghuid}没有邀请人,无分红") @@ -1887,7 +1888,7 @@ class HuiyuanHuitiao(View): # 获取管事信息 try: - guanshi = UserGuanshi.objects.get(user__yonghuid=guanshi_id) + guanshi = UserGuanshi.query.get(user__yonghuid=guanshi_id) except UserGuanshi.DoesNotExist: logger.warning(f"管事信息不存在: {guanshi_id}") return @@ -1901,7 +1902,7 @@ class HuiyuanHuitiao(View): nicheng = dashou.nicheng or "未知打手" # 查询该打手针对该管事的已分红次数 - existing_count = Gsfenhong.objects.filter( + existing_count = Gsfenhong.query.filter( dashouid=order.yonghuid, guanshi=guanshi_id ).count() @@ -1921,7 +1922,7 @@ class HuiyuanHuitiao(View): if zuzhang_id: # 获取组长信息 try: - zuzhang = UserZuzhang.objects.get(user__yonghuid=zuzhang_id) + zuzhang = UserZuzhang.query.get(user__yonghuid=zuzhang_id) except UserZuzhang.DoesNotExist: logger.warning(f"组长信息不存在: {zuzhang_id}") zuzhang = None @@ -1965,7 +1966,7 @@ class HuiyuanHuitiao(View): logger.info(f"组长分红成功: 组长{zuzhang_id}, 金额{zuzhang_fenhong}") # 创建分红记录 - Gsfenhong.objects.create( + Gsfenhong.query.create( dingdan_id=order.dingdan_id, guanshi=guanshi_id, dashouid=order.yonghuid, @@ -2030,7 +2031,7 @@ class HuiyuanHuitiao(View): """计算管事分红金额""" # 先查多次分红配置表 try: - config = DuociFenhong.objects.get( + config = DuociFenhong.query.get( huiyuan=huiyuan, yonghuid=guanshi_id, cishu=cishu @@ -2055,7 +2056,7 @@ class HuiyuanHuitiao(View): return None # 查多次分红配置表 try: - config = DuociFenhong.objects.get( + config = DuociFenhong.query.get( huiyuan=huiyuan, yonghuid=zuzhang_id, cishu=cishu @@ -2141,7 +2142,7 @@ class HuiyuanShibai(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -2206,7 +2207,7 @@ class HuiyuanLunxun(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid ) @@ -2233,10 +2234,10 @@ class HuiyuanLunxun(APIView): # 5. 查询会员信息 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=order.huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id) # 6. 查询会员购买记录 - goumai_record = Huiyuangoumai.objects.filter( + goumai_record = Huiyuangoumai.query.filter( yonghu_id=request.user.yonghuid, huiyuan_id=order.huiyuan_id ).first() @@ -2350,7 +2351,7 @@ class ShangjiaChongzhi(APIView): # 4. 创建充值记录订单 try: - chongzhi_jilu = Czjilu.objects.create( + chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付状态 yonghuid=request.user.yonghuid, @@ -2577,7 +2578,7 @@ class ShangjiaHuitiao(View): # 4. 查询订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) + order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到商家订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: logger.error(f"商家订单不存在: {out_trade_no}") @@ -2607,7 +2608,7 @@ class ShangjiaHuitiao(View): update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.objects.get_or_create( + szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ 'zongsy': Decimal('0.00'), @@ -2644,7 +2645,7 @@ class ShangjiaHuitiao(View): # 更新商家余额 try: # 通过用户ID查找商家(高效查询) - shangjia = UserShangjia.objects.get(user__yonghuid=order.yonghuid) + shangjia = UserShangjia.query.get(user__yonghuid=order.yonghuid) # 增加余额 shangjia.yue += order_amount_yuan @@ -2746,7 +2747,7 @@ class ShangjiaShibai(APIView): # 2. 查询订单(确保是当前用户的订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=4 # 确保是商家充值订单 @@ -2812,7 +2813,7 @@ class ShangjiaChenggong(APIView): # 2. 查询订单(确保是当前用户的商家订单) try: - order = Czjilu.objects.get( + order = Czjilu.query.get( dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=4 # 商家充值类型 @@ -2895,13 +2896,13 @@ class GuanshiFenhongListAPI(APIView): # 先查询总数(使用分页查询优化) # 查询总记录数 - zong_tiaoshu = Gsfenhong.objects.filter(query).count() + zong_tiaoshu = Gsfenhong.query.filter(query).count() # 查询去重后的打手数量 - chongzhi_dashou_shuliang = Gsfenhong.objects.filter(query).values('dashouid').distinct().count() + chongzhi_dashou_shuliang = Gsfenhong.query.filter(query).values('dashouid').distinct().count() # 查询当前页数据,按创建时间倒序排列 - fenhong_list = Gsfenhong.objects.filter(query) \ + fenhong_list = Gsfenhong.query.filter(query) \ .order_by('-create_time') \ .values('dashouid', 'avatar', 'nicheng', 'fenhong', 'create_time')[offset:offset + page_size] @@ -3007,7 +3008,7 @@ class AdGetAllDataView(APIView): try: # 使用select_related或prefetch_related优化查询(如果需要关联查询) # 这里直接查询所有字段,然后提取需要的字段 - shangpinleixing_queryset = ShangpinLeixing.objects.all() + shangpinleixing_queryset = ShangpinLeixing.query.all() # 转换为前端需要的格式 shangpinleixing_list = [] @@ -3031,7 +3032,7 @@ class AdGetAllDataView(APIView): # 7. 查询商品专区数据 try: # 查询所有商品专区 - zhuanqu_queryset = ShangpinZhuanqu.objects.all() + zhuanqu_queryset = ShangpinZhuanqu.query.all() # 转换为前端需要的格式 zhuanqu_list = [] @@ -3052,7 +3053,7 @@ class AdGetAllDataView(APIView): # 8. 查询会员数据 try: # 查询所有会员 - huiyuan_queryset = Huiyuan.objects.all() + huiyuan_queryset = Huiyuan.query.all() # 转换为前端需要的格式 huiyuan_list = [] @@ -3137,7 +3138,7 @@ class ShangpinLeixingFabuView(APIView): # 1.3 验证管理员扩展表(使用AdminProfile模型) try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, @@ -3195,7 +3196,7 @@ class ShangpinLeixingFabuView(APIView): ) # 验证会员ID是否存在 try: - huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员ID不存在', 'data': None}, @@ -3337,7 +3338,7 @@ class ShangpinZhuanquFabuView(APIView): # 1.3 验证管理员扩展表(使用AdminProfile模型) try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, @@ -3364,7 +3365,7 @@ class ShangpinZhuanquFabuView(APIView): # 2.2 验证类型ID是否存在 try: # 验证商品类型是否存在 - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, @@ -3446,7 +3447,7 @@ class ShangpinFabuView(APIView): # 1.3 验证管理员扩展表(使用AdminProfile模型) try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, @@ -3535,7 +3536,7 @@ class ShangpinFabuView(APIView): # === 3. 验证关联数据 === # 3.1 验证商品类型 try: - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response( {'code': 400, 'msg': '商品类型不存在', 'data': None}, @@ -3563,7 +3564,7 @@ class ShangpinFabuView(APIView): # 验证会员是否存在 try: - huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, @@ -3606,7 +3607,7 @@ class ShangpinFabuView(APIView): # 3.3 验证商品专区 try: - zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id, leixing_id=leixing_id) + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id, leixing_id=leixing_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'msg': '商品专区不存在或与商品类型不匹配', 'data': None}, @@ -3750,7 +3751,7 @@ class GuizeTupianShangchuanView(APIView): # 1.3 验证管理员扩展表(使用AdminProfile模型) try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '管理员信息不存在', 'data': None}, @@ -3784,7 +3785,7 @@ class GuizeTupianShangchuanView(APIView): # === 3. 验证商品是否存在 === try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) + shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 400, 'msg': '商品不存在', 'data': None}, @@ -3893,7 +3894,7 @@ class AdHuiyuanHqView(APIView): # 1.3 验证管理员扩展表(使用AdminProfile模型) try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, @@ -3902,7 +3903,7 @@ class AdHuiyuanHqView(APIView): # === 2. 查询会员表数据 === # 查询所有会员数据,按创建时间倒序排列 - huiyuan_queryset = Huiyuan.objects.all().order_by('-create_time') + huiyuan_queryset = Huiyuan.query.all().order_by('-create_time') # 构建会员数据列表 huiyuan_list = [] @@ -3923,7 +3924,7 @@ class AdHuiyuanHqView(APIView): # === 3. 查询商品类型表数据 === # 筛选抢单要求类型等于数字1的所有数据(yaoqiuleixing=1) - shangpinleixing_queryset = ShangpinLeixing.objects.filter( + shangpinleixing_queryset = ShangpinLeixing.query.filter( yaoqiuleixing=1 # 抢单要求类型为1(会员抢单) ).order_by('id') # 按ID排序 @@ -4000,7 +4001,7 @@ class HuiyuanXiugaiView(APIView): # 1.3 验证管理员扩展表 try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, @@ -4017,7 +4018,7 @@ class HuiyuanXiugaiView(APIView): # === 3. 查询会员数据 === try: - huiyuan_obj = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan_obj = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response( {'code': 400, 'msg': '会员不存在', 'data': None}, @@ -4173,7 +4174,7 @@ class AdHuiyuanFbView(APIView): # 1.3 验证管理员扩展表 try: - admin_profile = AdminProfile.objects.get(user=user_main) + admin_profile = AdminProfile.query.get(user=user_main) except AdminProfile.DoesNotExist: return Response( {'code': 401, 'msg': '权限验证失败', 'data': None}, @@ -4264,7 +4265,7 @@ class AdHuiyuanFbView(APIView): random_part = ''.join(random.choices(chars, k=5)) huiyuan_id = f"V{random_part}" # 检查是否已存在 - if not Huiyuan.objects.filter(huiyuan_id=huiyuan_id).exists(): + if not Huiyuan.query.filter(huiyuan_id=huiyuan_id).exists(): return huiyuan_id huiyuan_id = generate_huiyuan_id() @@ -4360,15 +4361,15 @@ class AdShangpinHuQu(APIView): ) # 6. 查询公告数据(类型=1的公告) - gonggao_obj = Gonggao.objects.filter(leixing=1).first() + gonggao_obj = Gonggao.query.filter(leixing=1).first() shangpingonggao = gonggao_obj.jieshao if gonggao_obj else '' # 7. 查询轮播图数据(类型=1的轮播图) - lunbo_objs = Lunbo.objects.filter(leixing=1).values('tupian_url') + lunbo_objs = Lunbo.query.filter(leixing=1).values('tupian_url') shangpinlunbo = [item['tupian_url'] for item in lunbo_objs] # 8. 查询商品类型数据 - shangpinleixing_data = ShangpinLeixing.objects.all().values( + shangpinleixing_data = ShangpinLeixing.query.all().values( 'id', # 类型ID 'jieshao', # 类型介绍(类型名称) 'tupian_url', # 类型图片URL @@ -4378,21 +4379,21 @@ class AdShangpinHuQu(APIView): ) # 9. 查询商品专区数据 - shangpinzhuanqu_data = ShangpinZhuanqu.objects.all().values( + shangpinzhuanqu_data = ShangpinZhuanqu.query.all().values( 'id', # 专区ID 'mingzi', # 专区名称 'leixing_id' # 所属类型ID ) # 10. 查询会员数据 - huiyuan_data = Huiyuan.objects.all().values( + huiyuan_data = Huiyuan.query.all().values( 'huiyuan_id', # 会员ID 'jieshao', # 会员介绍(会员名称) 'jiage' # 会员价格 ) # 11. 查询商品数据 - shangpin_data = Shangpin.objects.all().values( + shangpin_data = Shangpin.query.all().values( 'id', # 商品ID 'biaoqian', # 商品标题/标签 'jiage', # 商品价格 @@ -4493,7 +4494,7 @@ class AdGonggaoXiuGai(APIView): ) # 8. 查询公告表,类型为1的公告 - gonggao_obj = Gonggao.objects.filter(leixing=1).first() + gonggao_obj = Gonggao.query.filter(leixing=1).first() # 如果不存在,则创建 if not gonggao_obj: @@ -4587,7 +4588,7 @@ class AdZhuanquXiuGai(APIView): # 3. 查询专区 try: - zhuanqu_obj = ShangpinZhuanqu.objects.get(id=zhuanqu_id) + zhuanqu_obj = ShangpinZhuanqu.query.get(id=zhuanqu_id) except ShangpinZhuanqu.DoesNotExist: return Response( {'code': 400, 'message': '专区不存在', 'data': None}, @@ -4683,7 +4684,7 @@ class AdLunboXiuGai(APIView): ) # 查询轮播图记录 - lunbo_obj = Lunbo.objects.filter(tupian_url=tupian_url, leixing=1).first() + lunbo_obj = Lunbo.query.filter(tupian_url=tupian_url, leixing=1).first() if not lunbo_obj: return Response( @@ -4758,7 +4759,7 @@ class AdLunboXiuGai(APIView): ) # 创建轮播图记录 - lunbo_obj = Lunbo.objects.create( + lunbo_obj = Lunbo.query.create( tupian_url=oss_file_path, # 存相对路径 leixing=1 ) @@ -4819,7 +4820,7 @@ class AdLeixingXiuGai(APIView): # 3. 查询商品类型 try: - leixing_obj = ShangpinLeixing.objects.get(id=leixing_id) + leixing_obj = ShangpinLeixing.query.get(id=leixing_id) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'message': '商品类型不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) @@ -4848,7 +4849,7 @@ class AdLeixingXiuGai(APIView): leixing_obj.huiyuan_id = huiyuan_id # 验证会员存在 try: - Huiyuan.objects.get(huiyuan_id=huiyuan_id) + Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) @@ -4876,7 +4877,7 @@ class AdLeixingXiuGai(APIView): if huiyuan_id and huiyuan_id != leixing_obj.huiyuan_id: # 验证会员存在 try: - Huiyuan.objects.get(huiyuan_id=huiyuan_id) + Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 400, 'msg': '会员ID不存在', 'data': None}, status=status.HTTP_400_BAD_REQUEST) @@ -4985,7 +4986,7 @@ class AdLeixingXiuGai(APIView): # 2. 执行批量更新(确保只有实际有变化的字段才会执行) if shangpin_update_fields: # 查询所有属于该类型的商品(使用filter而非all以提高效率) - affected_count = Shangpin.objects.filter( + affected_count = Shangpin.query.filter( leixing_id=leixing_obj.id ).update(**shangpin_update_fields) @@ -5088,7 +5089,7 @@ class AdShangpinNeiRongHuoQu(APIView): # 4. 查询商品数据 try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) + shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, @@ -5203,7 +5204,7 @@ class AdShangpinXiuGai(APIView): try: # 查询商品 try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) + shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, @@ -5251,7 +5252,7 @@ class AdShangpinXiuGai(APIView): try: # 查询商品 try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) + shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, @@ -5444,7 +5445,7 @@ class AdShangpinGuizeXiuGai(APIView): # 2. 查询商品 try: - shangpin_obj = Shangpin.objects.get(id=shangpin_id) + shangpin_obj = Shangpin.query.get(id=shangpin_id) except Shangpin.DoesNotExist: return Response( {'code': 404, 'message': '商品不存在', 'data': None}, @@ -5587,7 +5588,7 @@ class HuiyuanTypeListView(APIView): }, status=status.HTTP_401_UNAUTHORIZED) # 🔥 2. 获取所有会员类型 - huiyuan_list = Huiyuan.objects.all().order_by('create_time') + huiyuan_list = Huiyuan.query.all().order_by('create_time') # 🔥 3. 构建返回数据 huiyuan_data = [] @@ -5667,7 +5668,7 @@ class HuiyuanRecordListView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 🔥 4. 检查会员类型是否存在 - huiyuan_obj = Huiyuan.objects.filter(huiyuan_id=huiyuan_type).first() + huiyuan_obj = Huiyuan.query.filter(huiyuan_id=huiyuan_type).first() if not huiyuan_obj: return Response({ 'code': 404, @@ -5751,7 +5752,7 @@ class HuiyuanRecordListView(APIView): has_more = (page * pagesize) < total_count # 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数) - huiyuan_types = Huiyuan.objects.all() + huiyuan_types = Huiyuan.query.all() huiyuan_stats = [] for huiyuan in huiyuan_types: @@ -5850,7 +5851,7 @@ class ZuzhangFenhongView(APIView): # 1. 验证组长身份 try: - zuzhang = UserZuzhang.objects.get(user=current_user) + zuzhang = UserZuzhang.query.get(user=current_user) except UserZuzhang.DoesNotExist: return Response({ 'code': 403, @@ -5869,10 +5870,10 @@ class ZuzhangFenhongView(APIView): fenhong_zonge = zuzhang.fenyong_zonge # 3.2 邀请管事数(统计邀请人等于当前用户ID的管事记录) - yaoqing_guanshi = UserGuanshi.objects.filter(yaoqingren=current_user.yonghuid).count() + yaoqing_guanshi = UserGuanshi.query.filter(yaoqingren=current_user.yonghuid).count() # 3.3 分红次数(总记录数) - base_queryset = Gsfenhong.objects.filter(zuzhang_id=current_user.yonghuid).order_by('-create_time') + base_queryset = Gsfenhong.query.filter(zuzhang_id=current_user.yonghuid).order_by('-create_time') fenhong_cishu = base_queryset.count() # 4. 分页查询 @@ -5944,7 +5945,7 @@ class ZuzhangFenhongView(APIView): return {} # 查询用户主表,并预加载 boss_profile - users = UserMain.objects.filter(yonghuid__in=guanshi_ids).select_related( + users = UserMain.query.filter(yonghuid__in=guanshi_ids).select_related( 'boss_profile' ).only( 'yonghuid', 'avatar', @@ -6074,7 +6075,7 @@ class CzhqdyView(APIView): if leixing == 1: # 会员 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) target_price = huiyuan.jiage except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -6087,7 +6088,7 @@ class CzhqdyView(APIView): elif leixing == 4: # 罚款缴纳 try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=user.yonghuid) + fadan = Fadan.query.get(id=fadan_id, beichufa_id=user.yonghuid) target_price = fadan.fakuanjine except Fadan.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) @@ -6096,7 +6097,7 @@ class CzhqdyView(APIView): rates = {} for key in ['5', '6', '8', '11']: try: - rate_obj = Lilubiao.objects.get(fadanpingtai=key) + rate_obj = Lilubiao.query.get(fadanpingtai=key) rates[key] = rate_obj.lilu except Lilubiao.DoesNotExist: rates[key] = None # 未配置则跳过 @@ -6214,7 +6215,7 @@ class DsqrgmdhView(APIView): target_price = None if leixing == 1: try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) target_price = huiyuan.jiage except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -6226,14 +6227,14 @@ class DsqrgmdhView(APIView): if not fadan_id: return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) try: - fadan_obj = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) + fadan_obj = Fadan.query.get(id=fadan_id, beichufa_id=request.user.yonghuid) target_price = fadan_obj.fakuanjine except Fadan.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或不属于当前用户'}, status=status.HTTP_404_NOT_FOUND) identity = IDENTITY_CONFIG.get(shenfen_id) try: - rate_obj = Lilubiao.objects.get(fadanpingtai=identity['rate_key']) + rate_obj = Lilubiao.query.get(fadanpingtai=identity['rate_key']) rate = rate_obj.lilu if rate >= 1: return Response({'code': 400, 'msg': f"{identity['name']}费率异常"}, status=status.HTTP_400_BAD_REQUEST) @@ -6254,7 +6255,7 @@ class DsqrgmdhView(APIView): yonghuid = user.yonghuid try: - profile = identity['model'].objects.get(user=user) + profile = identity['model'].query.get(user=user) except identity['model'].DoesNotExist: return Response({'code': 403, 'msg': f'您不是{identity["name"]}身份'}) @@ -6292,7 +6293,7 @@ class DsqrgmdhView(APIView): shuoming = f"{identity['name']}抵扣 {action_desc},抵扣金额{required}元" - czjilu = Czjilu.objects.create( + czjilu = Czjilu.query.create( dingdan_id=dingdan_id, huiyuan_id=huiyuan_id if leixing == 1 else None, zhuangtai=1, @@ -6313,7 +6314,7 @@ class DsqrgmdhView(APIView): } if is_first_buy: try: - dashou = UserDashou.objects.get(user=user) + dashou = UserDashou.query.get(user=user) dashou.jifen += 10 dashou.save(update_fields=['jifen']) response_data['jifen'] = dashou.jifen @@ -6358,7 +6359,7 @@ class DsqrgmdhView(APIView): logger.error(f"事务处理异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': f'支付处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - final_profile = identity['model'].objects.get(user=user) + final_profile = identity['model'].query.get(user=user) response_data['balance'] = str(getattr(final_profile, identity['balance_field'])) return Response({ @@ -6374,7 +6375,7 @@ class DsqrgmdhView(APIView): def _process_member_purchase(self, yonghuid, huiyuan_id, huiyuan_obj): is_first_buy_any = False try: - record = Huiyuangoumai.objects.filter( + record = Huiyuangoumai.query.filter( yonghu_id=yonghuid, huiyuan_id=huiyuan_id ).first() @@ -6384,10 +6385,10 @@ class DsqrgmdhView(APIView): record.huiyuan_zhuangtai = 1 record.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai']) else: - other_count = Huiyuangoumai.objects.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count() + other_count = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count() if other_count == 0: is_first_buy_any = True - record = Huiyuangoumai.objects.create( + record = Huiyuangoumai.query.create( yonghu_id=yonghuid, huiyuan_id=huiyuan_id, jieshao=huiyuan_obj.jieshao, @@ -6397,6 +6398,6 @@ class DsqrgmdhView(APIView): return record, is_first_buy_any except Exception as e: if 'unique' in str(e).lower() or 'duplicate' in str(e).lower(): - record = Huiyuangoumai.objects.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id) + record = Huiyuangoumai.query.get(yonghu_id=yonghuid, huiyuan_id=huiyuan_id) return record, False raise \ No newline at end of file diff --git a/rank/models.py b/rank/models.py index 8be36ad..a2aac2b 100644 --- a/rank/models.py +++ b/rank/models.py @@ -1,9 +1,10 @@ from django.db import models +from gvsdsdk.model_base import QModel from django.conf import settings from orders.models import Dingdan # ==================== 板块表 ==================== -class Bankuai(models.Model): +class Bankuai(QModel): bankuai_id = models.AutoField(primary_key=True, verbose_name='板块ID') mingcheng = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='板块名称') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') @@ -18,7 +19,7 @@ class Bankuai(models.Model): # ==================== 打手订单表现记录 ==================== -class DashouBiaoxian(models.Model): +class DashouBiaoxian(QModel): dingdan = models.ForeignKey('orders.Dingdan', to_field='dingdan_id', on_delete=models.CASCADE, related_name='biaoxian', verbose_name='关联订单', db_index=True) dashou_id = models.CharField(max_length=32, db_index=True, verbose_name='打手ID') @@ -38,7 +39,7 @@ class DashouBiaoxian(models.Model): # ==================== 经验变动记录 ==================== -class JingyanJilu(models.Model): +class JingyanJilu(QModel): yonghu_id = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') yonghu_leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='用户类型') bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', verbose_name='所属板块', db_index=True) @@ -59,7 +60,7 @@ class JingyanJilu(models.Model): # ==================== 称号主表 ==================== -class Chenghao(models.Model): +class Chenghao(QModel): mingcheng = models.CharField(max_length=50, unique=True, verbose_name='称号名称') leixing = models.CharField(max_length=10, choices=[('dashou','打手'),('boss','老板'),('shangjia','商家'),('guanshi','管事'),('zuzhang','组长')], verbose_name='称号类型') bankuai = models.ForeignKey(Bankuai, on_delete=models.PROTECT, to_field='bankuai_id', null=True, blank=True, verbose_name='所属板块(空=全板块通用)') @@ -77,7 +78,7 @@ class Chenghao(models.Model): # ==================== 用户 - 称号关联 ==================== -class YonghuChenghao(models.Model): +class YonghuChenghao(QModel): yonghu = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chenghao_list', verbose_name='用户', db_index=True) chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='称号') huode_time = models.DateTimeField(auto_now_add=True, verbose_name='获得时间') @@ -94,7 +95,7 @@ class YonghuChenghao(models.Model): -class DingdanBiaoqian(models.Model): +class DingdanBiaoqian(QModel): """订单需求标签关联表""" dingdan = models.ForeignKey( Dingdan, @@ -116,7 +117,7 @@ class DingdanBiaoqian(models.Model): -class ShenheJilu(models.Model): +class ShenheJilu(QModel): """审核记录表""" shenqingren_id = models.CharField(max_length=7, verbose_name='申请人用户ID') shenheguan_id = models.CharField(max_length=7, null = True,verbose_name='审核官用户ID') @@ -144,7 +145,7 @@ class ShenheJilu(models.Model): -class KaohePayTemp(models.Model): +class KaohePayTemp(QModel): """考核支付临时上下文(仅用于支付成功后回调取参,不删除数据)""" dingdan_id = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='订单ID') yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') @@ -167,7 +168,7 @@ class KaohePayTemp(models.Model): -class KaoheCishuFeiyong(models.Model): +class KaoheCishuFeiyong(QModel): """各标签各考核次数所需缴纳金额""" chenghao = models.ForeignKey(Chenghao, on_delete=models.CASCADE, verbose_name='标签') cishu = models.IntegerField(verbose_name='第几次考核') @@ -181,7 +182,7 @@ class KaoheCishuFeiyong(models.Model): indexes = [models.Index(fields=['chenghao', 'cishu'])] -class KaoheJujueJilu(models.Model): +class KaoheJujueJilu(QModel): """考核拒绝原因记录""" jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='审核记录') cishu = models.IntegerField(verbose_name='对应次数') @@ -196,7 +197,7 @@ class KaoheJujueJilu(models.Model): indexes = [models.Index(fields=['jilu_id', 'cishu'])] -class KaoheJiluFeiyong(models.Model): +class KaoheJiluFeiyong(QModel): """每次考核记录实际缴纳的费用明细""" jilu = models.ForeignKey(ShenheJilu, on_delete=models.CASCADE, to_field='jilu_id', verbose_name='考核记录') cishu = models.IntegerField(verbose_name='第几次考核') @@ -210,7 +211,7 @@ class KaoheJiluFeiyong(models.Model): -class KaoheguanBankuai(models.Model): +class KaoheguanBankuai(QModel): """考核官与板块多对多关联表""" kaoheguan_id = models.CharField(max_length=7, db_index=True, verbose_name='考核官用户ID') bankuai = models.ForeignKey(Bankuai, on_delete=models.CASCADE, to_field='bankuai_id', verbose_name='所属板块') diff --git a/shop/models.py b/shop/models.py index e55cbe5..d3b8572 100644 --- a/shop/models.py +++ b/shop/models.py @@ -1,7 +1,8 @@ from django.db import models +from gvsdsdk.model_base import QModel -class YonghuPingzheng(models.Model): +class YonghuPingzheng(QModel): id = models.AutoField(primary_key=True, verbose_name='自增主键ID') yonghu = models.CharField( max_length=7, @@ -21,7 +22,7 @@ class YonghuPingzheng(models.Model): verbose_name_plural = verbose_name -class Dianpu(models.Model): +class Dianpu(QModel): id = models.AutoField(primary_key=True, verbose_name='店铺自增ID') pingzheng = models.OneToOneField( 'shop.YonghuPingzheng', # 同应用内可简写,但完整路径更清晰 @@ -68,7 +69,7 @@ class Dianpu(models.Model): -class DianpuMorenPeizhi(models.Model): +class DianpuMorenPeizhi(QModel): id = models.PositiveSmallIntegerField(primary_key=True, default=1, editable=False) moren_choucheng_feilv = models.DecimalField(max_digits=5, decimal_places=4, default=0.1000, verbose_name='默认订单抽成费率(小数,0.1000=10%)') moren_tixian_xiane = models.DecimalField(max_digits=10, decimal_places=2, default=1000.00, verbose_name='默认可提现限额') @@ -91,7 +92,7 @@ class DianpuMorenPeizhi(models.Model): -class DianpuShouzhiMeiriTongji(models.Model): +class DianpuShouzhiMeiriTongji(QModel): dianpu_id = models.PositiveIntegerField(db_index=True, verbose_name='店铺ID') riqi = models.DateField(db_index=True, verbose_name='统计日期') nian = models.PositiveSmallIntegerField(verbose_name='年份') @@ -144,7 +145,7 @@ class DianpuShouzhiMeiriTongji(models.Model): -class YonghuDianpuBangding(models.Model): +class YonghuDianpuBangding(QModel): id = models.AutoField(primary_key=True, verbose_name='绑定自增ID') yonghu = models.ForeignKey( 'users.UserMain', # 🔥 完整路径 @@ -164,7 +165,7 @@ class YonghuDianpuBangding(models.Model): unique_together = [['yonghu', 'dianpu']] -class ShangpinLeixingDianpu(models.Model): +class ShangpinLeixingDianpu(QModel): """ 店铺级商品类型映射表 与公共商品类型表 ShangpinLeixing 强制关联,用于店铺自定义类型 @@ -207,7 +208,7 @@ class ShangpinLeixingDianpu(models.Model): # shangdian/models.py 或独立应用 models.py -class DianpuShangpinShenheShezhi(models.Model): +class DianpuShangpinShenheShezhi(QModel): """店铺商品审核模式全局配置(仅一条记录)""" kaiqi_shenhe = models.BooleanField(default=False, verbose_name='是否开启商品审核模式') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') diff --git a/shop/views.py b/shop/views.py index 0583b04..6a8f734 100644 --- a/shop/views.py +++ b/shop/views.py @@ -19,6 +19,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from django.conf import settings from django.db import models, transaction, IntegrityError from django.db.models import F, Sum, Count, Q, Prefetch +from gvsdsdk.fluent import db, func, FQ from django.core.cache import cache from django.core.paginator import Paginator, EmptyPage from django.core.exceptions import ObjectDoesNotExist @@ -73,13 +74,13 @@ class BindDianpuView(APIView): # 先获取 UserMain 对象(不加锁,仅用于校验存在性) try: - user_main = UserMain.objects.get(yonghuid=user.yonghuid) + user_main = UserMain.query.get(yonghuid=user.yonghuid) except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '用户不存在'}) # 校验店铺是否存在且状态正常(不加锁) try: - dianpu = Dianpu.objects.get(id=dianpu_id) + dianpu = Dianpu.query.get(id=dianpu_id) except Dianpu.DoesNotExist: return Response({'code': 404, 'msg': '店铺不存在'}) @@ -113,7 +114,7 @@ class BindDianpuView(APIView): existing_binding.save() except YonghuDianpuBangding.DoesNotExist: - YonghuDianpuBangding.objects.create(yonghu=user_main_locked, dianpu=dianpu_locked) + YonghuDianpuBangding.query.create(yonghu=user_main_locked, dianpu=dianpu_locked) # 新店铺绑定人数加1 dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1 @@ -142,12 +143,12 @@ class ShopGoodsView(APIView): def post(self, request): user = request.user try: - user_main = UserMain.objects.get(yonghuid=user.yonghuid) + user_main = UserMain.query.get(yonghuid=user.yonghuid) except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '用户不存在'}) # 1. 查询用户店铺绑定关系 - binding = YonghuDianpuBangding.objects.filter( + binding = YonghuDianpuBangding.query.filter( yonghu=user_main ).select_related('dianpu').first() @@ -171,10 +172,10 @@ class ShopGoodsView(APIView): def _has_special_identity(self, user_main): """判断用户是否拥有打手/商家/管事/组长任一身份""" - return (UserDashou.objects.filter(user=user_main).exists() or - UserShangjia.objects.filter(user=user_main).exists() or - UserGuanshi.objects.filter(user=user_main).exists() or - UserZuzhang.objects.filter(user=user_main).exists()) + return (UserDashou.query.filter(user=user_main).exists() or + UserShangjia.query.filter(user=user_main).exists() or + UserGuanshi.query.filter(user=user_main).exists() or + UserZuzhang.query.filter(user=user_main).exists()) def _get_shop_goods(self, dianpu): """ @@ -182,7 +183,7 @@ class ShopGoodsView(APIView): 类型、专区、商品均从店铺相关表中获取,且需通过平台审核(shenhe_zhuangtai=True) """ # 1. 店铺商品类型:未封禁、已上架、平台审核通过 - leixing_queryset = ShangpinLeixingDianpu.objects.filter( + leixing_queryset = ShangpinLeixingDianpu.query.filter( dianpu=dianpu, fengjin_zhuangtai=False, shangjia_zhuangtai=True, @@ -202,14 +203,14 @@ class ShopGoodsView(APIView): # 2. 专区:根据店铺和店铺类型ID,未封禁、已上架 if leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.objects.filter( + zhuanqu_queryset = ShangpinZhuanqu.query.filter( dianpu=dianpu, dianpu_leixing_id__in=leixing_ids, fengjin_zhuangtai=False, shangjia_zhuangtai=True ).order_by('-paixu', 'id') else: - zhuanqu_queryset = ShangpinZhuanqu.objects.none() + zhuanqu_queryset = ShangpinZhuanqu.query.none() zhuanqu_data = [] for zq in zhuanqu_queryset: @@ -223,7 +224,7 @@ class ShopGoodsView(APIView): # 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常 zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] if zhuanqu_ids: - shangpin_queryset = Shangpin.objects.filter( + shangpin_queryset = Shangpin.query.filter( dianpu=dianpu, dianpu_leixing_id__in=leixing_ids, zhuanqu_id__in=zhuanqu_ids, @@ -260,7 +261,7 @@ class ShopGoodsView(APIView): target_shenhe = 1 if use_audit_status_one else 2 # 1. 公共商品类型:原有审核状态为 target_shenhe - leixing_queryset = ShangpinLeixing.objects.filter( + leixing_queryset = ShangpinLeixing.query.filter( shenhezhuangtai=target_shenhe ).order_by('-paixu', 'id') @@ -277,14 +278,14 @@ class ShopGoodsView(APIView): # 2. 专区:dianpu 为空,且 dianpu_leixing 为空,leixing_id 在可见范围内,原有审核状态为 target_shenhe if visible_leixing_ids: - zhuanqu_queryset = ShangpinZhuanqu.objects.filter( + zhuanqu_queryset = ShangpinZhuanqu.query.filter( dianpu__isnull=True, dianpu_leixing__isnull=True, # 🔥 必须为空 leixing_id__in=visible_leixing_ids, shenhezhuangtai=target_shenhe ).order_by('-paixu', 'id') else: - zhuanqu_queryset = ShangpinZhuanqu.objects.none() + zhuanqu_queryset = ShangpinZhuanqu.query.none() zhuanqu_data = [] for zq in zhuanqu_queryset: @@ -298,7 +299,7 @@ class ShopGoodsView(APIView): # 3. 商品:dianpu 为空,dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe zhuanqu_ids = [zq['id'] for zq in zhuanqu_data] if zhuanqu_ids: - shangpin_queryset = Shangpin.objects.filter( + shangpin_queryset = Shangpin.query.filter( dianpu__isnull=True, dianpu_leixing__isnull=True, # 🔥 必须为空 leixing_id__in=visible_leixing_ids, @@ -350,14 +351,14 @@ class ShangdianLoginView(APIView): # 3. 验证用户主表是否存在 try: - user_main = UserMain.objects.get(yonghuid=yonghuid) + user_main = UserMain.query.get(yonghuid=yonghuid) except UserMain.DoesNotExist: logger.warning(f"登录失败:用户ID不存在 - {yonghuid}") return Response({'code': 404, 'msg': '账号或密码错误'}) # 4. 通过用户ID + 账号查询凭证表 try: - pingzheng = YonghuPingzheng.objects.get( + pingzheng = YonghuPingzheng.query.get( yonghu=yonghuid, zhanghao=zhanghao, is_active=True @@ -530,7 +531,7 @@ class DianpuShouzhiTongjiView(APIView): month = request.data.get('month') # 查询当前店铺的统计记录 - qs = DianpuShouzhiMeiriTongji.objects.filter(dianpu_id=dianpu.id) + qs = DianpuShouzhiMeiriTongji.query.filter(dianpu_id=dianpu.id) if granularity == 'day': if not year or not month: @@ -790,7 +791,7 @@ class ShopProductConfigView(APIView): # ---------- 2. 公共商品类型(需审核正常) ---------- try: - public_types_qs = ShangpinLeixing.objects.filter(shenhezhuangtai=1).values( + public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( 'id', 'jieshao', 'tupian_url' ) public_types = [{ @@ -804,7 +805,7 @@ class ShopProductConfigView(APIView): # ---------- 3. 店铺商品类型 ---------- try: - shop_types = ShangpinLeixingDianpu.objects.filter( + shop_types = ShangpinLeixingDianpu.query.filter( dianpu_id=dianpu.id ).select_related('gonggong_leixing').order_by('-paixu') except Exception as e: @@ -829,7 +830,7 @@ class ShopProductConfigView(APIView): # ---------- 4. 各类型商品数量统计 ---------- type_product_counts = {} if type_ids: - counts = Shangpin.objects.filter( + counts = Shangpin.query.filter( dianpu_id=dianpu.id, dianpu_leixing_id__in=type_ids ).values('dianpu_leixing_id').annotate(cnt=Count('id')) @@ -837,7 +838,7 @@ class ShopProductConfigView(APIView): # ---------- 5. 专区列表 ---------- try: - zhuanqu_qs = ShangpinZhuanqu.objects.filter( + zhuanqu_qs = ShangpinZhuanqu.query.filter( dianpu_id=dianpu.id ).order_by('-paixu').values( 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai', @@ -862,7 +863,7 @@ class ShopProductConfigView(APIView): # ---------- 6. 各专区商品数量统计 ---------- zhuanqu_product_counts = {} if zhuanqu_ids: - counts = Shangpin.objects.filter( + counts = Shangpin.query.filter( dianpu_id=dianpu.id, zhuanqu_id__in=zhuanqu_ids ).values('zhuanqu_id').annotate(cnt=Count('id')) @@ -932,7 +933,7 @@ class ShopProductModifyView(APIView): return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'}) # 验证公共类型存在且正常 - if not ShangpinLeixing.objects.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): + if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '所选公共商品类型无效'}) image_file = request.FILES.get('image') @@ -949,10 +950,10 @@ class ShopProductModifyView(APIView): tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) # 审核状态 - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() shenhe_status = not (audit_config and audit_config.kaiqi_shenhe) - ShangpinLeixingDianpu.objects.create( + ShangpinLeixingDianpu.query.create( dianpu_id=dianpu.id, gonggong_leixing_id=gonggong_leixing_id, jieshao=jieshao, @@ -997,7 +998,7 @@ class ShopProductModifyView(APIView): # 映射公共类型 if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST: new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') - if not ShangpinLeixing.objects.filter(id=new_id, shenhezhuangtai=1).exists(): + if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '公共商品类型无效'}) obj.gonggong_leixing_id = new_id update_fields.append('gonggong_leixing_id') @@ -1031,7 +1032,7 @@ class ShopProductModifyView(APIView): # 审核重置 if content_changed: - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: obj.shenhe_zhuangtai = False update_fields.append('shenhe_zhuangtai') @@ -1066,7 +1067,7 @@ class ShopProductModifyView(APIView): if delete_products: # 删除该类型下所有关联商品(同时清理图片) - products = Shangpin.objects.filter( + products = Shangpin.query.filter( dianpu_leixing_id=obj.id, dianpu_id=dianpu.id ) for p in products: @@ -1077,7 +1078,7 @@ class ShopProductModifyView(APIView): p.delete() else: # 仅解除关联,不删商品 - Shangpin.objects.filter( + Shangpin.query.filter( dianpu_leixing_id=obj.id, dianpu_id=dianpu.id ).update(dianpu_leixing=None) @@ -1104,7 +1105,7 @@ class ShopProductModifyView(APIView): # 获取店铺商品类型,并拿到映射的公共类型ID try: - shop_type = ShangpinLeixingDianpu.objects.get( + shop_type = ShangpinLeixingDianpu.query.get( id=dianpu_leixing_id, dianpu_id=dianpu.id ) leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id @@ -1112,10 +1113,10 @@ class ShopProductModifyView(APIView): return Response({'code': 400, 'msg': '所选商品类型无效'}) # 验证公共类型有效 - if not ShangpinLeixing.objects.filter(id=leixing_id, shenhezhuangtai=1).exists(): + if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) - ShangpinZhuanqu.objects.create( + ShangpinZhuanqu.query.create( dianpu_id=dianpu.id, mingzi=mingzi, dianpu_leixing_id=dianpu_leixing_id, @@ -1156,7 +1157,7 @@ class ShopProductModifyView(APIView): if 'dianpu_leixing_id' in request.data: new_id = request.data.get('dianpu_leixing_id') try: - shop_type = ShangpinLeixingDianpu.objects.get( + shop_type = ShangpinLeixingDianpu.query.get( id=new_id, dianpu_id=dianpu.id ) obj.dianpu_leixing_id = new_id @@ -1205,7 +1206,7 @@ class ShopProductModifyView(APIView): delete_products = str(delete_products).lower() in ('true', '1', 'yes') if delete_products: - products = Shangpin.objects.filter( + products = Shangpin.query.filter( zhuanqu_id=obj.id, dianpu_id=dianpu.id ) for p in products: @@ -1215,7 +1216,7 @@ class ShopProductModifyView(APIView): delete_from_oss(p.guize_tupian) p.delete() else: - Shangpin.objects.filter( + Shangpin.query.filter( zhuanqu_id=obj.id, dianpu_id=dianpu.id ).update(zhuanqu_id=None) @@ -1271,7 +1272,7 @@ class ShopProductListView(APIView): fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) # 3. 构建查询(只查当前店铺的商品) - products = Shangpin.objects.filter(dianpu_id=dianpu.id).select_related( + products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related( 'dianpu_leixing' ).only( 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id', @@ -1432,7 +1433,7 @@ class ShopProductModifyView1(APIView): # 店铺商品类型 if 'dianpu_leixing_id' in data: new_type_id = to_int(data['dianpu_leixing_id']) - if not ShangpinLeixingDianpu.objects.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): + if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): return Response({'code': 400, 'msg': '无效的店铺商品类型'}) product.dianpu_leixing_id = new_type_id update_fields.append('dianpu_leixing_id') @@ -1443,7 +1444,7 @@ class ShopProductModifyView1(APIView): new_zq_id = data['zhuanqu_id'] if new_zq_id is not None and new_zq_id != '': new_zq_id = to_int(new_zq_id) - if not ShangpinZhuanqu.objects.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): + if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): return Response({'code': 400, 'msg': '无效的专区'}) product.zhuanqu_id = new_zq_id else: @@ -1482,10 +1483,10 @@ class ShopProductModifyView1(APIView): else: # 自动从公共类型获取会员ID,或转为佣金 try: - shop_type = ShangpinLeixingDianpu.objects.get( + shop_type = ShangpinLeixingDianpu.query.get( id=product.dianpu_leixing_id, dianpu_id=dianpu.id ) - public_type = ShangpinLeixing.objects.get(id=shop_type.gonggong_leixing_id) + public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id) auto_huiyuan_id = public_type.huiyuan_id if auto_huiyuan_id: product.huiyuan_id = auto_huiyuan_id @@ -1581,7 +1582,7 @@ class ShopProductModifyView1(APIView): content_changed = True if content_changed: - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: product.shenhe_zhuangtai = False update_fields.append('shenhe_zhuangtai') @@ -1604,12 +1605,12 @@ class ShopProductModifyView1(APIView): return Response({'code': 400, 'msg': '必填项缺失'}) try: - shop_type = ShangpinLeixingDianpu.objects.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) + shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 400, 'msg': '无效的店铺商品类型'}) try: - public_type = ShangpinLeixing.objects.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) + public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) @@ -1672,11 +1673,11 @@ class ShopProductModifyView1(APIView): return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})''' shenhe_status = True - audit_config = DianpuShangpinShenheShezhi.objects.filter(id=1).first() + audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: shenhe_status = False - product = Shangpin.objects.create( + product = Shangpin.query.create( biaoqian=biaoqian, jiage=jiage, kucun=kucun, @@ -1745,7 +1746,7 @@ class MemberListView(APIView): # ---------- 3. 查询所有会员(只返回前端需要的字段) ---------- try: - members = Huiyuan.objects.all().values('huiyuan_id', 'jieshao') + members = Huiyuan.query.all().values('huiyuan_id', 'jieshao') member_list = [ { 'huiyuan_id': m['huiyuan_id'], @@ -1796,7 +1797,7 @@ class SubShopListView(APIView): page_size = request.data.get('page_size', 10) # 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID - queryset = Dianpu.objects.filter(yaoqing_dianpu_id=parent_dianpu.id) + queryset = Dianpu.query.filter(yaoqing_dianpu_id=parent_dianpu.id) # 4. 筛选条件 if dianpu_mingcheng: @@ -1910,13 +1911,13 @@ class SubShopModifyView(APIView): return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'}) # 检查用户是否存在 - if not UserMain.objects.filter(yonghuid=user_uid).exists(): + if not UserMain.query.filter(yonghuid=user_uid).exists(): return Response({'code': 404, 'msg': '对应的小程序用户不存在'}) # 检查账号是否已占用 - if YonghuPingzheng.objects.filter(zhanghao=zhanghao).exists(): + if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): return Response({'code': 400, 'msg': '登录账号已存在'}) - if YonghuPingzheng.objects.filter(yonghu=user_uid).exists(): + if YonghuPingzheng.query.filter(yonghu=user_uid).exists(): return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'}) # 费率处理:前端可选,后端默认 0.0500 @@ -1933,14 +1934,14 @@ class SubShopModifyView(APIView): try: with transaction.atomic(): # 创建凭证 - pingzheng = YonghuPingzheng.objects.create( + pingzheng = YonghuPingzheng.query.create( yonghu=user_uid, zhanghao=zhanghao, mima=mima, is_active=True ) # 创建店铺,关联上级 - dianpu = Dianpu.objects.create( + dianpu = Dianpu.query.create( pingzheng=pingzheng, dianpu_mingcheng=dianpu_mingcheng, lianxi_dianhua=lianxi_dianhua or '', @@ -2100,7 +2101,7 @@ class ShopOrderStatisticsView(APIView): try: # 2. 获取所有正常状态(审核通过)的商品类型 - types_qs = ShangpinLeixing.objects.filter( + types_qs = ShangpinLeixing.query.filter( shenhezhuangtai=1 ).order_by('-paixu', 'id') @@ -2122,7 +2123,7 @@ class ShopOrderStatisticsView(APIView): # 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表) order_pks = list( - DingdanPingtai.objects.filter(dianpu_id=shop_id) + DingdanPingtai.query.filter(dianpu_id=shop_id) .values_list('dingdan_id', flat=True) # 这里取的是订单主表的主键id ) @@ -2217,7 +2218,7 @@ def _fetch_images(order, oss_domain): # 仅当订单有接单打手ID时才查询打手图片 if order.jiedan_dashou_id: # 从打手图片表中查询该订单、该打手的所有图片记录 - image_urls = Dashoutupian.objects.filter( + image_urls = Dashoutupian.query.filter( dingdan_id=order.dingdan_id, # 订单ID匹配 dashou=order.jiedan_dashou_id # 接单打手ID匹配 ).values_list('tupian', flat=True) # 只取图片URL字段 @@ -2323,7 +2324,7 @@ class ShopOrderManageView(APIView): # ------------------------------------------------------------ # 步骤1:获取所有正常的商品类型(审核状态为1) # ------------------------------------------------------------ - types_query = ShangpinLeixing.objects.filter( + types_query = ShangpinLeixing.query.filter( shenhezhuangtai=1 # 审核状态为1(正常) ).order_by('-paixu', 'id') # 按排序权重和ID排序 @@ -2351,7 +2352,7 @@ class ShopOrderManageView(APIView): # ------------------------------------------------------------ # 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID order_ids = list( - DingdanPingtai.objects.filter( + DingdanPingtai.query.filter( dianpu_id=shop_id # 店铺ID匹配 ).values_list('dingdan_id', flat=True) # 只取订单ID字段 ) @@ -2365,7 +2366,7 @@ class ShopOrderManageView(APIView): # ------------------------------------------------------------ if order_ids: # 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数 - aggregation = Dingdan.objects.filter( + aggregation = Dingdan.query.filter( dingdan_id__in=order_ids # 从该店铺的订单中查询 ).values( 'leixing_id', # 按类型ID分组 @@ -2432,7 +2433,7 @@ class ShopOrderManageView(APIView): page_size = request.data.get('page_size', 20) # 基础查询:从平台扩展表关联订单主表,并按创建时间倒序 - qs = DingdanPingtai.objects.filter( + qs = DingdanPingtai.query.filter( dianpu_id=shop_id ).select_related('dingdan').order_by('-dingdan__create_time') @@ -2552,7 +2553,7 @@ class ShopRefundView(APIView): # 3. 查询订单,必须属于本店铺(通过扩展表关联) try: # 通过扩展表确认归属,并 select_related 获取订单主表 - pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( dianpu_id=shop_id, dingdan__dingdan_id=dingdan_id ) @@ -2586,7 +2587,7 @@ class ShopRefundView(APIView): # 更新打手统计(退款量+1,状态置为空闲) if dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 @@ -2716,7 +2717,7 @@ class ShopRefundView(APIView): def _update_refund_record(self, order, dashou_id, chuli_id, reason): """更新退款记录表""" try: - refund_record, created = Tuikuanjilu.objects.get_or_create( + refund_record, created = Tuikuanjilu.query.get_or_create( dingdan_id=order.dingdan_id, defaults={ 'dashouid': dashou_id or '', @@ -2799,7 +2800,7 @@ class ShopForceCompleteView(APIView): # ========== 3. 查询订单并验证归属 ========== try: # 通过平台扩展表确认订单属于本店铺 - pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( dianpu_id=shop_id, dingdan__dingdan_id=dingdan_id ) @@ -2829,7 +2830,7 @@ class ShopForceCompleteView(APIView): # ---------- 更新打手余额和统计 ---------- try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.chengjiaozongliang += 1 dashou_profile.yue += dashou_fencheng @@ -2948,7 +2949,7 @@ class ShopTransferHallView(APIView): # ---------- 3. 验证订单归属 ---------- try: - pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( dianpu_id=shop_id, dingdan__dingdan_id=dingdan_id ) @@ -2973,11 +2974,11 @@ class ShopTransferHallView(APIView): # ---------- 6. 记录打手历史(原接口保留逻辑) ---------- if current_dashou_id: - last_history = OrderDashouHistory.objects.filter( + last_history = OrderDashouHistory.query.filter( dingdan_id=dingdan_id ).order_by('-times').first() next_times = (last_history.times + 1) if last_history else 1 - OrderDashouHistory.objects.create( + OrderDashouHistory.query.create( dingdan_id=dingdan_id, dashou_id=current_dashou_id, times=next_times, @@ -2987,7 +2988,7 @@ class ShopTransferHallView(APIView): # ---------- 7. 释放本地打手 + 退款统计 ---------- if current_dashou_id: try: - dashou_profile = UserDashou.objects.get(user__yonghuid=current_dashou_id) + dashou_profile = UserDashou.query.get(user__yonghuid=current_dashou_id) dashou_profile.zhuangtai = 1 # 设为空闲 dashou_profile.save() except ObjectDoesNotExist: @@ -3073,7 +3074,7 @@ class ShopFineApplyView(APIView): # ---------- 3. 验证订单归属 ---------- try: - pingtai_ext = DingdanPingtai.objects.select_related('dingdan').get( + pingtai_ext = DingdanPingtai.query.select_related('dingdan').get( dianpu_id=shop_id, dingdan__dingdan_id=dingdan_id ) @@ -3088,12 +3089,12 @@ class ShopFineApplyView(APIView): return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) # ---------- 5. 重复罚款检查 ---------- - if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): + if Fadan.query.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists(): return Response({'code': 400, 'msg': '该打手已被罚款过'}) # ---------- 6. 创建罚单 ---------- with transaction.atomic(): - Fadan.objects.create( + Fadan.query.create( beichufa_id=dashou_id, shenqing_chufa=str(shop_id), # 申请人记为店铺ID shenfen=1, # 被处罚人身份:1 打手 diff --git a/users/models.py b/users/models.py index 9b8fce7..f65d7bb 100644 --- a/users/models.py +++ b/users/models.py @@ -1,10 +1,11 @@ # models.py from django.db import models from django.contrib.auth.models import AbstractBaseUser +from gvsdsdk.model_base import QModel # ==================== 用户主表 ==================== -class UserMain(AbstractBaseUser): +class UserMain(AbstractBaseUser, QModel): # ID字段 id = models.AutoField(primary_key=True, verbose_name='主键ID') yonghuid = models.CharField(max_length=7, unique=True, db_index=True, verbose_name='用户ID') @@ -70,7 +71,7 @@ class UserMain(AbstractBaseUser): # 新增服务号用户表 -class OfficialAccountUser(models.Model): +class OfficialAccountUser(QModel): unionid = models.CharField(max_length=64, null =True,db_index=True, verbose_name='UnionID') official_openid = models.CharField(max_length=64, db_index=True, unique=True, verbose_name='服务号OpenID') subscribe_time = models.DateTimeField(auto_now_add=True, verbose_name='关注时间') @@ -88,7 +89,7 @@ class OfficialAccountUser(models.Model): # ==================== 老板扩展表 ==================== -class UserBoss(models.Model): +class UserBoss(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='boss_profile', verbose_name='关联用户') nickname = models.CharField(max_length=50, verbose_name='老板昵称') @@ -105,7 +106,7 @@ class UserBoss(models.Model): # ==================== 打手扩展表 ==================== -class UserDashou(models.Model): +class UserDashou(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='dashou_profile', verbose_name='关联用户') nicheng = models.CharField(max_length=50, null=True, verbose_name='打手昵称') @@ -149,7 +150,7 @@ class UserDashou(models.Model): # ==================== 商家扩展表 ==================== -class UserShangjia(models.Model): +class UserShangjia(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='shop_profile', verbose_name='关联用户') nicheng = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='商家昵称') @@ -178,7 +179,7 @@ class UserShangjia(models.Model): # ==================== 管事扩展表 ==================== -class UserGuanshi(models.Model): +class UserGuanshi(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='guanshi_profile', verbose_name='关联用户') yaoqingren = models.CharField(max_length=7,null=True, db_index=True, verbose_name='邀请人ID') @@ -219,7 +220,7 @@ class UserGuanshi(models.Model): -class UserZuzhang(models.Model): +class UserZuzhang(QModel): """组长扩展表(与UserMain一对一关联)""" user = models.OneToOneField( UserMain, @@ -273,7 +274,7 @@ class UserZuzhang(models.Model): def __str__(self): return f"组长 {self.user.yonghuid}" -class UserShenheguan(models.Model): +class UserShenheguan(QModel): """审核官扩展表(与UserMain一对一关联)""" user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='shenheguan_profile', verbose_name='关联用户', db_index=True) zhuangtai = models.IntegerField(choices=[(0, '禁用'), (1, '正常')], default=1, verbose_name='账号状态') @@ -294,7 +295,7 @@ class UserShenheguan(models.Model): # ==================== 管理员扩展表 ==================== -class AdminProfile(models.Model): +class AdminProfile(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='admin_profile', verbose_name='关联用户') password = models.CharField(max_length=255, verbose_name='密码') @@ -309,7 +310,7 @@ class AdminProfile(models.Model): indexes = [models.Index(fields=['user']), ] -class UserKefu(models.Model): +class UserKefu(QModel): user = models.OneToOneField(UserMain, on_delete=models.CASCADE, related_name='kefu_profile', verbose_name='关联用户') nicheng = models.CharField(max_length=50, verbose_name='客服昵称') erjimima = models.CharField(max_length=128, verbose_name='二级密码') @@ -335,7 +336,7 @@ class UserKefu(models.Model): -class Tixianjilu(models.Model): +class Tixianjilu(QModel): id = models.AutoField(primary_key=True, verbose_name='提现记录ID') yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='提现用户ID') avatar = models.CharField(max_length=500, null=True, blank=True, verbose_name='头像') # 用CharField @@ -380,7 +381,7 @@ class Tixianjilu(models.Model): -class TixianShenheJilu(models.Model): +class TixianShenheJilu(QModel): """ 提现审核记录表(自动打款新流程) 状态码与前端 TX_STATUS / Tixianjilu.zhuangtai 完全对标: @@ -440,7 +441,7 @@ class TixianShenheJilu(models.Model): -class TixianAutoRecord(models.Model): +class TixianAutoRecord(QModel): """ 自动提现记录表(微信自动打款) """ @@ -499,7 +500,7 @@ class TixianAutoRecord(models.Model): # yonghu/models.py -class Xiugaijilu(models.Model): +class Xiugaijilu(QModel): id = models.AutoField(primary_key=True, verbose_name='记录ID') yonghuid = models.CharField(max_length=7, verbose_name='被修改用户ID') xiugaiid = models.CharField(max_length=30, verbose_name='修改人账号ID') @@ -549,7 +550,7 @@ class Xiugaijilu(models.Model): # yonghu/models.py - 新增排行榜模型 -class RankingRecord(models.Model): +class RankingRecord(QModel): # id: 主键ID id = models.AutoField(primary_key=True, verbose_name='ID') diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index 86ec2f0..d754baa 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -51,6 +51,8 @@ from orders.utils import update_daily_payout from django.db.models import Sum +from gvsdsdk.fluent import db, func, FQ + logger = logging.getLogger('yonghu.tixian_shenhe') ORDER_STATUS_COMPLETED = 3 @@ -128,7 +130,7 @@ def load_audit_meta_map(audit_ids): if has_dakuan_mode_field(): value_fields.append('dakuan_mode') result = {} - for row in TixianShenheJilu.objects.filter(id__in=audit_ids).values(*value_fields): + for row in TixianShenheJilu.query.filter(id__in=audit_ids).values(*value_fields): result[row['id']] = { 'shenhe_danhao': row.get('shenhe_danhao') or '', 'dakuan_mode': row.get('dakuan_mode', DAKUAN_MODE_AUTO), @@ -142,7 +144,7 @@ def generate_shenhe_danhao(): timestamp = str(int(time.time() * 1000))[-10:] random_part = str(random.randint(1000, 9999)) danhao = f'TXSH{timestamp}{random_part}' - if not TixianShenheJilu.objects.filter(shenhe_danhao=danhao).exists(): + if not TixianShenheJilu.query.filter(shenhe_danhao=danhao).exists(): return danhao raise RuntimeError('生成审核单号失败,请重试') @@ -151,7 +153,7 @@ def get_tixian_feilv(leixing): rate_key = LEIXING_RATE_KEY.get(leixing) if not rate_key: raise ValueError('无效的提现类型') - rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() + rate_obj = Lilubiao.query.filter(fadanpingtai=rate_key).first() if not rate_obj: raise ValueError(f'提现费率未配置(类型{leixing})') return decimal.Decimal(str(rate_obj.lilu)) @@ -170,7 +172,7 @@ def check_dashou_has_valid_huiyuan(yonghuid): 返回 (True, '') 或 (False, 原因) """ - records = Huiyuangoumai.objects.filter(yonghu_id=yonghuid) + records = Huiyuangoumai.query.filter(yonghu_id=yonghuid) if not records.exists(): return False, '您暂无会员记录,无法提现' @@ -194,7 +196,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): if leixing == 1: try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法提现佣金', {} if dashou.zhanghaozhuangtai != 1: @@ -212,7 +214,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): if leixing == 2: try: - guanshi = UserGuanshi.objects.get(user=user_main) + guanshi = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '您不是管事身份,无法提现分红', {} if guanshi.zhuangtai != 1: @@ -223,7 +225,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): if leixing == 3: try: - zuzhang = UserZuzhang.objects.get(user=user_main) + zuzhang = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '您不是组长身份,无法提现组长分红', {} if zuzhang.zhuangtai != 1: @@ -234,7 +236,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): if leixing == 4: try: - kaoheguan = UserShenheguan.objects.get(user=user_main) + kaoheguan = UserShenheguan.query.get(user=user_main) except UserShenheguan.DoesNotExist: return False, '您不是考核官身份,无法提现分佣', {} if kaoheguan.zhuangtai != 1: @@ -245,7 +247,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine): if leixing == 5: try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法提取押金', {} if dashou.zhanghaozhuangtai != 1: @@ -259,34 +261,34 @@ def validate_withdraw_eligibility(user_main, leixing, jine): return False, huiyuan_msg, {} if dashou.yajin < jine: return False, f'押金余额不足,当前押金: {dashou.yajin}', {} - unfinished = Dingdan.objects.filter( + unfinished = Dingdan.query.filter( jiedan_dashou_id=yonghuid, ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished: return False, '您还有未完成或未退款的订单,请先处理完毕再提取押金', {} - self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} return True, '', {'nicheng': dashou.nicheng or ''} if leixing == 6: try: - shangjia = UserShangjia.objects.get(user=user_main) + shangjia = UserShangjia.query.get(user=user_main) except UserShangjia.DoesNotExist: return False, '您不是商家身份,无法提现商家余额', {} if shangjia.zhuangtai != 1: return False, '商家账号已被禁用,无法提现', {} if shangjia.yue < jine: return False, f'商家余额不足,当前余额: {shangjia.yue}', {} - unfinished_orders = DingdanShangjia.objects.filter( + unfinished_orders = DingdanShangjia.query.filter( shangjia_id=yonghuid, ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished_orders: return False, '您还有未完成或未退款的订单,请先处理完毕再提现余额', {} - self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理', {} - applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if applied_fadan: return False, '您发起的对其他用户的处罚尚未处理完毕,请等待完结', {} return True, '', {'nicheng': shangjia.nicheng or ''} @@ -320,7 +322,7 @@ def validate_collect_eligibility(user_main, leixing): if leixing == 1: try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法收款' if dashou.zhanghaozhuangtai != 1: @@ -336,7 +338,7 @@ def validate_collect_eligibility(user_main, leixing): if leixing == 5: try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '您不是打手身份,无法收款' if dashou.zhanghaozhuangtai != 1: @@ -348,19 +350,19 @@ def validate_collect_eligibility(user_main, leixing): huiyuan_ok, huiyuan_msg = check_dashou_has_valid_huiyuan(yonghuid) if not huiyuan_ok: return False, huiyuan_msg - unfinished = Dingdan.objects.filter( + unfinished = Dingdan.query.filter( jiedan_dashou_id=yonghuid, ).exclude(zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished: return False, '您还有未完成或未退款的订单,无法收款' - self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单,无法收款' return True, '' if leixing == 2: try: - guanshi = UserGuanshi.objects.get(user=user_main) + guanshi = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '您不是管事身份,无法收款' if guanshi.zhuangtai != 1: @@ -369,27 +371,27 @@ def validate_collect_eligibility(user_main, leixing): if leixing == 6: try: - shangjia = UserShangjia.objects.get(user=user_main) + shangjia = UserShangjia.query.get(user=user_main) except UserShangjia.DoesNotExist: return False, '您不是商家身份,无法收款' if shangjia.zhuangtai != 1: return False, '商家账号已被禁用,无法收款' - unfinished_orders = DingdanShangjia.objects.filter( + unfinished_orders = DingdanShangjia.query.filter( shangjia_id=yonghuid, ).exclude(dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]).exists() if unfinished_orders: return False, '您还有未完成或未退款的订单,无法收款' - self_fadan = Fadan.objects.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + self_fadan = Fadan.query.filter(beichufa_id=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if self_fadan: return False, '您有未处理完毕的自身罚单,无法收款' - applied_fadan = Fadan.objects.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() + applied_fadan = Fadan.query.filter(shenqing_chufa=yonghuid).exclude(zhuangtai__in=[2, 4]).exists() if applied_fadan: return False, '您发起的处罚尚未处理完毕,无法收款' return True, '' if leixing == 3: try: - zuzhang = UserZuzhang.objects.get(user=user_main) + zuzhang = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '您不是组长身份,无法收款' if zuzhang.zhuangtai != 1: @@ -398,7 +400,7 @@ def validate_collect_eligibility(user_main, leixing): if leixing == 4: try: - kaoheguan = UserShenheguan.objects.get(user=user_main) + kaoheguan = UserShenheguan.query.get(user=user_main) except UserShenheguan.DoesNotExist: return False, '您不是考核官身份,无法收款' if kaoheguan.zhuangtai != 1: @@ -413,14 +415,14 @@ def _get_personal_daily_quota(leixing, profile): if leixing == 1: if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: return profile.ewai_xiane - return TixianQuotaDefault.objects.get(leixing=1).default_quota + return TixianQuotaDefault.query.get(leixing=1).default_quota if leixing == 2: if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0: return profile.ewai_xiane - return TixianQuotaDefault.objects.get(leixing=2).default_quota + return TixianQuotaDefault.query.get(leixing=2).default_quota if leixing == 3: try: - default_quota = TixianQuotaDefault.objects.get(leixing=3).default_quota + default_quota = TixianQuotaDefault.query.get(leixing=3).default_quota except TixianQuotaDefault.DoesNotExist: default_quota = decimal.Decimal('0.00') if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0: @@ -433,7 +435,7 @@ def _get_active_auto_record(shenhe_danhao): if not shenhe_danhao: return None return ( - TixianAutoRecord.objects.filter( + TixianAutoRecord.query.filter( shenhe_danhao=shenhe_danhao, zhuangtai__in=[0, 1], ) @@ -447,7 +449,7 @@ def _get_platform_daily_total(leixing, today=None): if today is None: today = date.today() try: - return TixianRiTongji.objects.get(riqi=today, leixing=leixing).total_amount + return TixianRiTongji.query.get(riqi=today, leixing=leixing).total_amount except TixianRiTongji.DoesNotExist: return decimal.Decimal('0.00') @@ -480,7 +482,7 @@ def _get_platform_daily_limit(leixing): """ quota_leixing = leixing + 3 try: - return TixianQuotaDefault.objects.get(leixing=quota_leixing).default_quota + return TixianQuotaDefault.query.get(leixing=quota_leixing).default_quota except TixianQuotaDefault.DoesNotExist: logger.warning( '平台每日总限额未配置 leixing=%s(读取quota_leixing=%s),按0处理禁止新增收款', @@ -497,7 +499,7 @@ def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): today = date.today() if leixing == 1: try: - profile = UserDashou.objects.get(user=user_main) + profile = UserDashou.query.get(user=user_main) except UserDashou.DoesNotExist: return False, '打手信息不存在', 'personal' if profile.last_tixian_date != today: @@ -506,7 +508,7 @@ def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): personal_today = profile.jinritixian_jine elif leixing == 2: try: - profile = UserGuanshi.objects.get(user=user_main) + profile = UserGuanshi.query.get(user=user_main) except UserGuanshi.DoesNotExist: return False, '管事信息不存在', 'personal' if profile.last_tixian_date != today: @@ -515,7 +517,7 @@ def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''): personal_today = profile.jinritixian_jine else: try: - profile = UserZuzhang.objects.get(user=user_main) + profile = UserZuzhang.query.get(user=user_main) except UserZuzhang.DoesNotExist: return False, '组长信息不存在', 'personal' if profile.last_tixian_time and profile.last_tixian_time.date() == today: @@ -706,7 +708,7 @@ def release_collect_quota_for_record(auto_record): if not _quota_already_reserved_for_record(auto_record): return try: - user_main = UserMain.objects.get(yonghuid=auto_record.yonghuid) + user_main = UserMain.query.get(yonghuid=auto_record.yonghuid) except UserMain.DoesNotExist: logger.error('回滚限额预占失败:用户不存在 %s', auto_record.yonghuid) return @@ -726,7 +728,7 @@ def _quota_already_reserved_for_record(auto_record): today = date.today() shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') stat_total = _get_platform_daily_total(auto_record.leixing, today) - other_sum = TixianAutoRecord.objects.filter( + other_sum = TixianAutoRecord.query.filter( leixing=auto_record.leixing, zhuangtai__in=[0, 1], create_time__date=today, @@ -751,7 +753,7 @@ def apply_transfer_success_quota(auto_record): shijidaozhang = auto_record.shijidaozhang or decimal.Decimal('0.00') try: - user_main = UserMain.objects.get(yonghuid=auto_record.yonghuid) + user_main = UserMain.query.get(yonghuid=auto_record.yonghuid) except UserMain.DoesNotExist: logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) return @@ -879,25 +881,25 @@ def get_balance_response_data(user_main, leixing): data = {} if leixing == 1: try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) data['new_yongjin'] = str(dashou.yue.quantize(decimal.Decimal('0.01'))) except UserDashou.DoesNotExist: pass elif leixing == 2: try: - guanshi = UserGuanshi.objects.get(user=user_main) + guanshi = UserGuanshi.query.get(user=user_main) data['new_fenyong'] = str(guanshi.yue.quantize(decimal.Decimal('0.01'))) except UserGuanshi.DoesNotExist: pass elif leixing == 3: try: - zuzhang = UserZuzhang.objects.get(user=user_main) + zuzhang = UserZuzhang.query.get(user=user_main) data['new_zuzhang_ketixian'] = str(zuzhang.ketixian_jine.quantize(decimal.Decimal('0.01'))) except UserZuzhang.DoesNotExist: pass elif leixing == 4: try: - kg = UserShenheguan.objects.get(user=user_main) + kg = UserShenheguan.query.get(user=user_main) data['new_kaoheguan_yue'] = str(kg.yue.quantize(decimal.Decimal('0.01'))) except UserShenheguan.DoesNotExist: pass @@ -913,7 +915,7 @@ def sync_audit_and_jilu_status(audit, new_status, **extra_fields): update_fields = {'zhuangtai': new_status, 'update_time': timezone.now()} if 'bhliyou' in extra_fields: update_fields['bhliyou'] = extra_fields['bhliyou'] - Tixianjilu.objects.filter(id=audit.tixianjilu_id).update(**update_fields) + Tixianjilu.query.filter(id=audit.tixianjilu_id).update(**update_fields) def _close_auto_record_failed(auto_record, fail_reason): @@ -1037,7 +1039,7 @@ def _apply_platform_accounting(auto_record): szjilu.jrzc += auto_record.shijidaozhang szjilu.save() except Szjilu.DoesNotExist: - Szjilu.objects.create( + Szjilu.query.create( id=1, zongsy=-auto_record.shijidaozhang, zongzc=auto_record.shijidaozhang, @@ -1145,7 +1147,7 @@ def _sync_record_from_wx_query(auto_record, wx_data): release_collect_quota_for_record(auto_record) if auto_record.shenhe_danhao: try: - audit = TixianShenheJilu.objects.get(shenhe_danhao=auto_record.shenhe_danhao) + audit = TixianShenheJilu.query.get(shenhe_danhao=auto_record.shenhe_danhao) with transaction.atomic(): close_audit_wechat_failed(audit, None, fail_reason) except TixianShenheJilu.DoesNotExist: @@ -1168,14 +1170,14 @@ def get_auto_record_for_collect(shenhe_danhao): 优先 zhuangtai=0(进行中),否则取最新一条(可能本地误标失败但微信未终态) """ pending = ( - TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao, zhuangtai=0) .order_by('-create_time') .first() ) if pending: return pending return ( - TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao) + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao) .order_by('-create_time') .first() ) @@ -1188,12 +1190,12 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao): 返回 (action, payload) """ records = list( - TixianAutoRecord.objects.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') + TixianAutoRecord.query.filter(shenhe_danhao=shenhe_danhao).order_by('-create_time') ) if any(r.zhuangtai == 1 for r in records): try: - audit = TixianShenheJilu.objects.get(shenhe_danhao=shenhe_danhao) + audit = TixianShenheJilu.query.get(shenhe_danhao=shenhe_danhao) if audit.zhuangtai != 2: sync_audit_and_jilu_status(audit, 2) except TixianShenheJilu.DoesNotExist: @@ -1248,7 +1250,7 @@ def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shen jilu = None if tixianjilu_id is not None: try: - jilu = Tixianjilu.objects.get(id=int(tixianjilu_id), yonghuid=yonghuid) + jilu = Tixianjilu.query.get(id=int(tixianjilu_id), yonghuid=yonghuid) except (Tixianjilu.DoesNotExist, TypeError, ValueError): return None, '提现记录不存在或不属于当前用户' @@ -1272,13 +1274,13 @@ def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shen try: if shenhe_jilu_id: - audit = TixianShenheJilu.objects.get( + audit = TixianShenheJilu.query.get( id=int(shenhe_jilu_id), shenhe_danhao=shenhe_danhao, yonghuid=yonghuid, ) else: - audit = TixianShenheJilu.objects.get( + audit = TixianShenheJilu.query.get( shenhe_danhao=shenhe_danhao, yonghuid=yonghuid, ) @@ -1294,7 +1296,7 @@ def resolve_collect_context(yonghuid, tixianjilu_id=None, shenhe_danhao='', shen if jilu is None: try: - jilu = Tixianjilu.objects.get( + jilu = Tixianjilu.query.get( id=audit.tixianjilu_id, yonghuid=yonghuid, ) if audit.tixianjilu_id else None @@ -1310,7 +1312,7 @@ def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg= 返回 (action, payload_or_user_msg) """ try: - ar = TixianAutoRecord.objects.get(tixian_id=tixian_id) + ar = TixianAutoRecord.query.get(tixian_id=tixian_id) except TixianAutoRecord.DoesNotExist: return RECONCILE_PENDING, '收款处理中,请稍后再试' @@ -1402,9 +1404,9 @@ def create_audit_application(user_main, leixing, jine): } if has_dakuan_mode_field(): audit_kwargs['dakuan_mode'] = DAKUAN_MODE_AUTO - audit = TixianShenheJilu.objects.create(**audit_kwargs) + audit = TixianShenheJilu.query.create(**audit_kwargs) - tixian_record = Tixianjilu.objects.create( + tixian_record = Tixianjilu.query.create( yonghuid=yonghuid, avatar=user_main.avatar or '', phone=user_main.phone or '', diff --git a/users/tixian_shenhe_views.py b/users/tixian_shenhe_views.py index 7d671df..00088db 100644 --- a/users/tixian_shenhe_views.py +++ b/users/tixian_shenhe_views.py @@ -21,6 +21,7 @@ from rest_framework.views import APIView from utils.redis_lock import acquire_lock, release_lock from utils.wechat_v3 import build_authorization +from gvsdsdk.fluent import db, func, FQ from .models import TixianAutoRecord from .tixian_shenhe_services import ( @@ -87,7 +88,7 @@ def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao): def _mark_auto_record_failed(tixian_id, fail_reason): """微信明确失败时标记打款记录终态,避免后续一直被当成「处理中」""" - TixianAutoRecord.objects.filter(tixian_id=tixian_id, zhuangtai=0).update( + TixianAutoRecord.query.filter(tixian_id=tixian_id, zhuangtai=0).update( zhuangtai=2, fail_reason=(fail_reason or '打款失败')[:500], update_time=timezone.now(), @@ -294,7 +295,7 @@ def process_audit_collect(request): quota_reserved = True tixian_id = generate_tixian_id() - TixianAutoRecord.objects.create( + TixianAutoRecord.query.create( tixian_id=tixian_id, yonghuid=yonghuid, leixing=leixing, @@ -317,7 +318,7 @@ def process_audit_collect(request): if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM': package_info = wx_result.get('package_info') - auto_record = TixianAutoRecord.objects.get(tixian_id=tixian_id) + auto_record = TixianAutoRecord.query.get(tixian_id=tixian_id) auto_record.wechat_package = json.dumps(package_info, ensure_ascii=False) auto_record.save(update_fields=['wechat_package']) @@ -344,7 +345,7 @@ def process_audit_collect(request): _mark_auto_record_failed(tixian_id, err_msg) if quota_reserved: release_collect_quota_for_record( - TixianAutoRecord.objects.get(tixian_id=tixian_id), + TixianAutoRecord.query.get(tixian_id=tixian_id), ) quota_reserved = False return Response({ @@ -378,7 +379,7 @@ def process_audit_collect(request): _mark_auto_record_failed(tixian_id, user_msg) if quota_reserved: release_collect_quota_for_record( - TixianAutoRecord.objects.get(tixian_id=tixian_id), + TixianAutoRecord.query.get(tixian_id=tixian_id), ) quota_reserved = False return Response({'code': 99, 'msg': user_msg}) diff --git a/users/views.py b/users/views.py index 33bc2ca..1d17071 100644 --- a/users/views.py +++ b/users/views.py @@ -39,6 +39,8 @@ from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication +from gvsdsdk.fluent import db, func, FQ + from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.wechat_v3 import verify_wechat_sign, decrypt_callback_ciphertext @@ -215,7 +217,7 @@ class WechatMiniProgramLoginView(APIView): user_main.save() if created: - UserBoss.objects.create(user=user_main, nickname='板板大人') + UserBoss.query.create(user=user_main, nickname='板板大人') # 检查扩展表 dashou_status = 0 @@ -224,32 +226,32 @@ class WechatMiniProgramLoginView(APIView): boss_nickname = '微信用户' try: - boss_profile = UserBoss.objects.get(user=user_main) + boss_profile = UserBoss.query.get(user=user_main) boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户' except UserBoss.DoesNotExist: pass try: - UserDashou.objects.get(user=user_main) + UserDashou.query.get(user=user_main) dashou_status = 1 except UserDashou.DoesNotExist: dashou_status = None try: - UserShangjia.objects.get(user=user_main) + UserShangjia.query.get(user=user_main) shangjia_status = 1 except UserShangjia.DoesNotExist: shangjia_status = None try: - UserGuanshi.objects.get(user=user_main) + UserGuanshi.query.get(user=user_main) guanshi_status = 1 except UserGuanshi.DoesNotExist: guanshi_status = None try: - UserZuzhang.objects.get(user=user_main) + UserZuzhang.query.get(user=user_main) zuzhang_status = 1 except UserZuzhang.DoesNotExist: zuzhang_status = None @@ -257,7 +259,7 @@ class WechatMiniProgramLoginView(APIView): # 在检查 zuzhang 扩展表之后,插入以下代码 try: - kg = UserShenheguan.objects.get(user=user_main) + kg = UserShenheguan.query.get(user=user_main) kaoheguan_status = 1 if kg.zhuangtai == 1 else 0 except UserShenheguan.DoesNotExist: kaoheguan_status = 0 @@ -340,7 +342,7 @@ class WechatMiniProgramLoginView(APIView): user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): + if not UserMain.query.filter(yonghuid=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -363,12 +365,12 @@ class WechatMiniProgramLoginView(APIView): 统计订单数量 """ try: - if not DingdanPingtai.objects.filter(laoban_id=yonghuid).exists(): + if not DingdanPingtai.query.filter(laoban_id=yonghuid).exists(): return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0} - dingdan_ids = DingdanPingtai.objects.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True) + dingdan_ids = DingdanPingtai.query.filter(laoban_id=yonghuid).values_list('dingdan__id', flat=True) - results = Dingdan.objects.filter(id__in=dingdan_ids).aggregate( + results = Dingdan.query.filter(id__in=dingdan_ids).aggregate( daifuwu=Count(Case(When(Q(zhuangtai=1) | Q(zhuangtai=7), then=1), output_field=IntegerField())), fuwuzhong=Count(Case(When(zhuangtai=2, then=1), output_field=IntegerField())), yiwancheng=Count(Case(When(zhuangtai=3, then=1), output_field=IntegerField())), @@ -390,7 +392,7 @@ class WechatMiniProgramLoginView(APIView): 获取群配置信息 """ try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', @@ -497,7 +499,7 @@ class WeixinOfficialCallbackView(APIView): logger.info(f"用户关注,OpenID: {from_user}, UnionID: {unionid}") # 核心逻辑:更新或创建记录 - obj, created = OfficialAccountUser.objects.update_or_create( + obj, created = OfficialAccountUser.query.update_or_create( official_openid=from_user, defaults={ 'unionid': unionid, @@ -520,7 +522,7 @@ class WeixinOfficialCallbackView(APIView): def handle_unsubscribe_event(self, root, from_user): """处理用户取消关注事件 - 简化版:标记为未订阅""" try: - updated_count = OfficialAccountUser.objects.filter( + updated_count = OfficialAccountUser.query.filter( official_openid=from_user ).update(is_subscribed=False) @@ -672,9 +674,9 @@ class UserInfoUpdateView(APIView): # ---------- 工具 ---------- def _get_or_create_boss(self, user): try: - return UserBoss.objects.get(user=user) + return UserBoss.query.get(user=user) except UserBoss.DoesNotExist: - return UserBoss.objects.create(user=user, nickname='微信用户') + return UserBoss.query.create(user=user, nickname='微信用户') def _upload_avatar(self, user, file): try: @@ -737,7 +739,7 @@ class DashouXinxiAPIView(APIView): # 4. 查询会员信息(原有逻辑) huiyuan_list = [] try: - huiyuan_records = Huiyuangoumai.objects.filter( + huiyuan_records = Huiyuangoumai.query.filter( yonghu_id=yonghuid ).select_related() @@ -799,7 +801,7 @@ class HuoQuYaoQingRenView(APIView): # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态) try: - dashou = UserDashou.objects.get(user=current_user) + dashou = UserDashou.query.get(user=current_user) if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 403, @@ -824,7 +826,7 @@ class HuoQuYaoQingRenView(APIView): # 3. 查询邀请人主表(头像) try: - inviter_main = UserMain.objects.get(yonghuid=yaoqingren_id) + inviter_main = UserMain.query.get(yonghuid=yaoqingren_id) touxiang = inviter_main.avatar or '' except UserMain.DoesNotExist: return Response({ @@ -836,7 +838,7 @@ class HuoQuYaoQingRenView(APIView): # 4. 查询老板扩展表(昵称),如果没有则使用空字符串 nicheng = '' try: - boss = UserBoss.objects.get(user=inviter_main) + boss = UserBoss.query.get(user=inviter_main) nicheng = boss.nickname or '' except UserBoss.DoesNotExist: pass # 邀请人可能不是老板,昵称留空 @@ -988,11 +990,11 @@ class GuanshiXinxiView(APIView): # 查询管事扩展表 try: - guanshi_profile = UserGuanshi.objects.get(user=current_user) + guanshi_profile = UserGuanshi.query.get(user=current_user) # 🔥【新增】查询已充值打手数量(从分红表中统计) # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid) - yichongzhi_count = Gsfenhong.objects.filter(guanshi=current_user.yonghuid).count() + yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.yonghuid).count() # 返回管事信息 return Response({ @@ -1064,7 +1066,7 @@ class ShangJiaXinXiView(APIView): # ------------------- 2. 获取称号列表(仅限商家称号) ------------------- chenghao_list = [] # 查询当前用户的所有称号关联 - yonghu_chenghao_qs = YonghuChenghao.objects.filter(yonghu=user).select_related('chenghao') + yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao') for yc in yonghu_chenghao_qs: chenghao_obj = yc.chenghao # 只保留类型为“商家”的称号 @@ -1078,7 +1080,7 @@ class ShangJiaXinXiView(APIView): # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- try: - today_stat = ShangjiaRiTongji.objects.get(yonghuid=yonghuid, riqi=today) + today_stat = ShangjiaRiTongji.query.get(yonghuid=yonghuid, riqi=today) jinri_paifa_dingdan = today_stat.paifa_dingdan_shu jinri_paifa_jine = float(today_stat.paifa_jine) jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu @@ -1092,7 +1094,7 @@ class ShangJiaXinXiView(APIView): response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- - month_stats = ShangjiaRiTongji.objects.filter( + month_stats = ShangjiaRiTongji.query.filter( yonghuid=yonghuid, riqi__gte=this_month_start, riqi__lte=today @@ -1303,7 +1305,7 @@ class DashouZhuceView(APIView): # 3. 根据邀请码查找对应的管事 try: # 使用select_related一次性获取管事及其关联的主表用户信息,性能最高 - guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) + guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma) except UserGuanshi.DoesNotExist: return Response({ 'code': 404, @@ -1320,7 +1322,7 @@ class DashouZhuceView(APIView): guanshi_yonghuid = guanshi_profile.user.yonghuid # 5.1 创建打手扩展表 - dashou_profile = UserDashou.objects.create( + dashou_profile = UserDashou.query.create( user=current_user, nicheng='大手子', # 默认昵称 chenghao='普通大手', # 默认称号 @@ -1410,7 +1412,7 @@ class DashouZhuceView(APIView): # 使用当前用户的yonghuid进行查询 huiyuan_goumai_list = [] if not is_new: # 如果是新用户,直接返回空列表,无需查询 - goumai_records = Huiyuangoumai.objects.filter(yonghu_id=current_user.yonghuid) + goumai_records = Huiyuangoumai.query.filter(yonghu_id=current_user.yonghuid) for record in goumai_records: # 调用方法检查是否到期,此方法内部会更新状态 shifou_daoqi = record.jiance_shifou_daoqi() @@ -1465,7 +1467,7 @@ class GuanshiYaoqingDashouListView(APIView): zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选:1=在线,2=离线 # 3. 基础查询:当前管事邀请的所有打手 - base_qs = UserDashou.objects.select_related('user').filter( + base_qs = UserDashou.query.select_related('user').filter( yaoqingren=guanshi_yonghuid ) @@ -1576,11 +1578,11 @@ class TixianXinxiHuoquView(APIView): # 🔥【修改开始 - 使用Decimal,字段名是lilu】 # 查询打手提现费率 (fadanpingtai='5') - dashou_rate_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + dashou_rate_obj = Lilubiao.query.filter(fadanpingtai='5').first() dashou_rate = decimal.Decimal(str(dashou_rate_obj.lilu)) if dashou_rate_obj else decimal.Decimal('0.00') # 查询管事提现费率 (fadanpingtai='6') - guanshi_rate_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + guanshi_rate_obj = Lilubiao.query.filter(fadanpingtai='6').first() guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.lilu)) if guanshi_rate_obj else decimal.Decimal('0.00') # 🔥【修改结束】 @@ -1846,7 +1848,7 @@ class TixianShenqingView(APIView): }.get(leixing) if rate_key: - rate_obj = Lilubiao.objects.filter(fadanpingtai=rate_key).first() + rate_obj = Lilubiao.query.filter(fadanpingtai=rate_key).first() if rate_obj: current_rate = decimal.Decimal(str(rate_obj.lilu)) else: @@ -1940,7 +1942,7 @@ class TixianShenqingView(APIView): phone = user_main.phone or '' zhifu = user_main.zhifu or '' skzhanghao = user_main.skzhanghao or '' - tixian_record = Tixianjilu.objects.create( + tixian_record = Tixianjilu.query.create( yonghuid=yonghuid, avatar=avatar, phone=phone, @@ -1986,7 +1988,7 @@ class TixianShenqingView(APIView): def _check_dashou_commission_withdraw(self, user_main, jine): """打手佣金提现校验""" try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'} @@ -2004,7 +2006,7 @@ class TixianShenqingView(APIView): def _check_guanshi_dividend_withdraw(self, user_main, jine): """管事分红提现校验""" try: - guanshi = UserGuanshi.objects.get(user=user_main) + guanshi = UserGuanshi.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 20, 'msg': '您不是管事身份,无法提现分红'} if guanshi.zhuangtai != 1: @@ -2016,7 +2018,7 @@ class TixianShenqingView(APIView): def _check_zuzhang_withdraw(self, user_main, jine): """组长分红提现校验""" try: - zuzhang = UserZuzhang.objects.get(user=user_main) + zuzhang = UserZuzhang.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'} if zuzhang.zhuangtai != 1: @@ -2028,7 +2030,7 @@ class TixianShenqingView(APIView): def _check_shenheguan_withdraw(self, user_main, jine): """审核官分佣提现校验""" try: - shenheguan = UserShenheguan.objects.get(user=user_main) + shenheguan = UserShenheguan.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'} if shenheguan.zhuangtai != 1: @@ -2044,7 +2046,7 @@ class TixianShenqingView(APIView): 包含:订单、自身罚单、申请的罚单检查 """ try: - shangjia = UserShangjia.objects.get(user=user_main) + shangjia = UserShangjia.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'} @@ -2054,7 +2056,7 @@ class TixianShenqingView(APIView): return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'} # 订单检查(使用 Q 取反) - unfinished_orders = DingdanShangjia.objects.filter( + unfinished_orders = DingdanShangjia.query.filter( shangjia_id=user_main.yonghuid, ).exclude( dingdan__zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL] @@ -2063,7 +2065,7 @@ class TixianShenqingView(APIView): return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'} # 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4)) - self_fadan = Fadan.objects.filter( + self_fadan = Fadan.query.filter( beichufa_id=user_main.yonghuid, ).exclude( zhuangtai__in=[2, 4] @@ -2072,7 +2074,7 @@ class TixianShenqingView(APIView): return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'} # 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4)) - applied_fadan = Fadan.objects.filter( + applied_fadan = Fadan.query.filter( shenqing_chufa=user_main.yonghuid, ).exclude( zhuangtai__in=[2, 4] @@ -2088,7 +2090,7 @@ class TixianShenqingView(APIView): 要求:无进行中的订单,无未结算订单,自身罚单需已处理 """ try: - dashou = UserDashou.objects.get(user=user_main) + dashou = UserDashou.query.get(user=user_main) except ObjectDoesNotExist: return {'code': 50, 'msg': '您不是打手身份,无法提取押金'} @@ -2100,7 +2102,7 @@ class TixianShenqingView(APIView): return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'} # 订单检查(使用 exclude + __in) - unfinished_orders = Dingdan.objects.filter( + unfinished_orders = Dingdan.query.filter( jiedan_dashou_id=user_main.yonghuid, ).exclude( zhuangtai__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED] @@ -2109,7 +2111,7 @@ class TixianShenqingView(APIView): return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'} # 自身罚单检查 - self_fadan = Fadan.objects.filter( + self_fadan = Fadan.query.filter( beichufa_id=user_main.yonghuid, ).exclude( zhuangtai__in=[2, 4] @@ -2158,7 +2160,7 @@ class TixianJiluHuoquViewV2(APIView): limit = 100 offset = (page - 1) * limit - qs = Tixianjilu.objects.filter(yonghuid=yonghuid) + qs = Tixianjilu.query.filter(yonghuid=yonghuid) if mode == 2: # 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选 if zhuangtai_filter > 0: @@ -2179,7 +2181,7 @@ class TixianJiluHuoquViewV2(APIView): audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')] audit_map = {} if audit_ids: - for a in TixianShenheJilu.objects.filter(id__in=audit_ids).values( + for a in TixianShenheJilu.query.filter(id__in=audit_ids).values( 'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao' ): audit_map[a['id']] = a @@ -2188,7 +2190,7 @@ class TixianJiluHuoquViewV2(APIView): danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')] auto_fail_map = {} if danhao_list: - for ar in TixianAutoRecord.objects.filter( + for ar in TixianAutoRecord.query.filter( shenhe_danhao__in=danhao_list, zhuangtai=2 ).order_by('-create_time').values('shenhe_danhao', 'fail_reason'): if ar['shenhe_danhao'] not in auto_fail_map: @@ -2301,7 +2303,7 @@ class AdminLoginView(APIView): with transaction.atomic(): # 【优化点1】使用select_related一次性获取管理员扩展表 # 只查询管理员类型的用户 - user = UserMain.objects.filter( + user = UserMain.query.filter( phone=zhanghao, # 手机号作为账号 password=mima, # 密码 user_type='admin' # 必须是管理员类型 @@ -2442,7 +2444,7 @@ class AdminFinancialDataView(APIView): try: # 使用select_related一次性获取管理员扩展表 # 注意:这里假设user_type='admin'的用户都有admin_profile - yonghu_xiangxi = UserMain.objects.select_related('admin_profile').get( + yonghu_xiangxi = UserMain.query.select_related('admin_profile').get( pk=dangqianyonghu.pk, user_type='admin' ) @@ -2466,11 +2468,11 @@ class AdminFinancialDataView(APIView): # 如果需要汇总多条记录,可以使用aggregate进行聚合查询 # 方法1:获取最新的一条记录(假设表中只有一条汇总记录) - shouzhijilu = Szjilu.objects.order_by('-create_time').first() + shouzhijilu = Szjilu.query.order_by('-create_time').first() # 方法2:如果表中可能有多条记录,需要汇总,可以使用以下方式: # from django.db.models Sum - # zonghe = Szjilu.objects.aggregate( + # zonghe = Szjilu.query.aggregate( # zongliushui=Sum('zongls'), # zongshouyi=Sum('zongsy'), # zongzhichu=Sum('zongzc'), @@ -2577,7 +2579,7 @@ class AdGetOrderTypes(APIView): try: # 请替换为你的实际模型名 # 只查询前端需要的三个字段 - shangpin_types = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') + shangpin_types = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url') # 构建响应数据 type_list = [ @@ -2718,12 +2720,12 @@ class AdGetDingdanList(APIView): # 注意:这里统计所有订单,不考虑筛选条件 try: # 平台订单统计 (fadan_pingtai=1) - platform_total = Dingdan.objects.filter(fadan_pingtai=1).count() - platform_completed = Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count() + platform_total = Dingdan.query.filter(fadan_pingtai=1).count() + platform_completed = Dingdan.query.filter(fadan_pingtai=1, zhuangtai=3).count() # 商家订单统计 (fadan_pingtai=2) - merchant_total = Dingdan.objects.filter(fadan_pingtai=2).count() - merchant_completed = Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count() + merchant_total = Dingdan.query.filter(fadan_pingtai=2).count() + merchant_completed = Dingdan.query.filter(fadan_pingtai=2, zhuangtai=3).count() platform_stats = { 'total': platform_total, @@ -2745,10 +2747,10 @@ class AdGetDingdanList(APIView): offset = (page - 1) * page_size # 查询符合条件的订单总数 - total_count = Dingdan.objects.filter(query_conditions).count() + total_count = Dingdan.query.filter(query_conditions).count() # 查询当前页的数据(只查询主表,只查询前端需要的字段) - dingdan_list = Dingdan.objects.filter(query_conditions).only( + dingdan_list = Dingdan.query.filter(query_conditions).only( 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', 'jine', 'tupian', 'create_time' ).order_by('-create_time')[offset:offset + page_size] @@ -2876,7 +2878,7 @@ class AdYaoQingDaShou(APIView): # 管事扩展表不存在,创建一个 try: # 创建默认的管事扩展表 - guanshi_profile = UserGuanshi.objects.create( + guanshi_profile = UserGuanshi.query.create( user=dangqianyonghu, yaoqingma='', # 暂时为空,后面生成 dianhua='', # 默认空 @@ -3081,10 +3083,10 @@ class AdGuanLiYongHu(APIView): try: stats = { - '1': UserBoss.objects.count(), - '2': UserDashou.objects.count(), - '3': UserGuanshi.objects.count(), - '4': UserShangjia.objects.count() + '1': UserBoss.query.count(), + '2': UserDashou.query.count(), + '3': UserGuanshi.query.count(), + '4': UserShangjia.query.count() } return stats @@ -3120,7 +3122,7 @@ class AdGuanLiYongHu(APIView): def _query_boss_users(self, keyword, offset, limit): """查询普通用户(老板)""" try: - queryset = UserBoss.objects.select_related('user') + queryset = UserBoss.query.select_related('user') if keyword: queryset = queryset.filter( @@ -3162,7 +3164,7 @@ class AdGuanLiYongHu(APIView): def _query_dashou_users(self, keyword, offset, limit): """查询打手用户""" try: - queryset = UserDashou.objects.select_related('user') + queryset = UserDashou.query.select_related('user') if keyword: queryset = queryset.filter( @@ -3208,7 +3210,7 @@ class AdGuanLiYongHu(APIView): def _query_guanshi_users(self, keyword, offset, limit): """查询管事用户""" try: - queryset = UserGuanshi.objects.select_related('user') + queryset = UserGuanshi.query.select_related('user') if keyword: queryset = queryset.filter( @@ -3250,7 +3252,7 @@ class AdGuanLiYongHu(APIView): def _query_shangjia_users(self, keyword, offset, limit): """查询商家用户""" try: - queryset = UserShangjia.objects.select_related('user') + queryset = UserShangjia.query.select_related('user') if keyword: queryset = queryset.filter( @@ -3365,9 +3367,9 @@ class CfGuanLi(APIView): ) # 7. 获取统计信息(总是需要统计) - zongshu = Chufajilu.objects.count() - daichuli = Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count() - yichuli = Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count() + zongshu = Chufajilu.query.count() + daichuli = Chufajilu.query.filter(sqzhuangtai__in=[0, 3]).count() + yichuli = Chufajilu.query.filter(sqzhuangtai__in=[1, 2]).count() # 8. 如果只请求统计信息,直接返回 if qingqiu_tongji: @@ -3382,7 +3384,7 @@ class CfGuanLi(APIView): }) # 9. 构建查询集 - queryset = Chufajilu.objects.all() + queryset = Chufajilu.query.all() # 10. 根据状态筛选 if sqzhuangtai is not None: @@ -3431,7 +3433,7 @@ class CfGuanLi(APIView): query_conditions |= Q(dingdan_id=dingdan_id, yonghuid=yonghuid) # 批量查询 - tupian_queryset = Chufatupian.objects.filter(query_conditions).values( + tupian_queryset = Chufatupian.query.filter(query_conditions).values( 'dingdan_id', 'yonghuid', 'tupian' ) @@ -3604,14 +3606,14 @@ class AdTongYiChuFa(APIView): with transaction.atomic(): # 8. 🔴 修复:查询处罚记录(处理重复记录问题) # 使用 filter().first() 而不是 get(),以防出现多条记录 - chufajilu = Chufajilu.objects.filter( + chufajilu = Chufajilu.query.filter( dingdan_id=dingdan_id, sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中状态 ).order_by('-create_time').first() # 取最新的一条 if not chufajilu: # 如果没有待处理的处罚记录,检查是否有已处理的 - exists_processed = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() + exists_processed = Chufajilu.query.filter(dingdan_id=dingdan_id).exists() if exists_processed: return Response( {'code': 400, 'message': '该处罚记录已处理', 'data': None}, @@ -3631,7 +3633,7 @@ class AdTongYiChuFa(APIView): ) # 10. 🔴 修复:检查是否有重复的待处理/申诉中记录,并记录警告 - duplicate_count = Chufajilu.objects.filter( + duplicate_count = Chufajilu.query.filter( dingdan_id=dingdan_id, sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 ).count() @@ -3642,7 +3644,7 @@ class AdTongYiChuFa(APIView): # 🔴 可选:将其他重复的待处理/申诉中记录标记为已拒绝 # 防止用户重复提交相同订单的处罚申请 - duplicate_records = Chufajilu.objects.filter( + duplicate_records = Chufajilu.query.filter( dingdan_id=dingdan_id, sqzhuangtai__in=[0, 3] # 🔴【修改】包括待处理和申诉中 ).exclude(id=chufajilu.id) @@ -3665,7 +3667,7 @@ class AdTongYiChuFa(APIView): # 11.2 查询打手扩展表(只是验证存在性) try: - dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) + dashou = UserDashou.query.get(user__yonghuid=chufajilu.dashouid) except UserDashou.DoesNotExist: return Response( {'code': 404, 'message': '被处罚打手不存在', 'data': None}, @@ -3679,7 +3681,7 @@ class AdTongYiChuFa(APIView): # 11.4 更新商家订单扩展表 try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): shangjia_kuozhan = dingdan.shangjia_kuozhan shangjia_kuozhan.sqzhuangtai = 1 # 已处罚 @@ -3695,7 +3697,7 @@ class AdTongYiChuFa(APIView): # 🔴【新增】11.5 返还打手积分(加5分) if chufajilu.jifen > 0: try: - dashou = UserDashou.objects.get(user__yonghuid=chufajilu.dashouid) + dashou = UserDashou.query.get(user__yonghuid=chufajilu.dashouid) # 🔴【新增】安全验证:必须积分小于等于5分才允许加分 if dashou.jifen <= 5: # 判断积分是否小于等于5分 @@ -3719,7 +3721,7 @@ class AdTongYiChuFa(APIView): # 11.7 更新商家订单扩展表 try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): shangjia_kuozhan = dingdan.shangjia_kuozhan shangjia_kuozhan.sqzhuangtai = 2 # 已拒绝 @@ -3824,7 +3826,7 @@ class AdckyhxqView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 查询用户主表 - 使用select_related优化查询 - user_main = UserMain.objects.filter(yonghuid=uid).first() + user_main = UserMain.query.filter(yonghuid=uid).first() if not user_main: return Response({ 'code': 404, @@ -3853,7 +3855,7 @@ class AdckyhxqView(APIView): # 根据用户类型查询扩展信息 if user_type == 1: # 老板 # 使用select_related预取boss_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('boss_profile').first() + user_main = UserMain.query.filter(yonghuid=uid).select_related('boss_profile').first() if user_main.boss_profile: boss_profile = user_main.boss_profile user_info.update({ @@ -3866,7 +3868,7 @@ class AdckyhxqView(APIView): elif user_type == 2: # 打手 # 使用select_related预取dashou_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('dashou_profile').first() + user_main = UserMain.query.filter(yonghuid=uid).select_related('dashou_profile').first() if user_main.dashou_profile: dashou_profile = user_main.dashou_profile @@ -3897,7 +3899,7 @@ class AdckyhxqView(APIView): # 查询打手的会员信息(只返回未过期的) # 使用select_related预取huiyuan信息(如果有关联的话) - member_records = Huiyuangoumai.objects.filter( + member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-create_time') @@ -3908,7 +3910,7 @@ class AdckyhxqView(APIView): if not is_expired: # 只返回未过期的 # 查询会员详情 - huiyuan = Huiyuan.objects.filter( + huiyuan = Huiyuan.query.filter( huiyuan_id=record.huiyuan_id ).first() @@ -3925,7 +3927,7 @@ class AdckyhxqView(APIView): elif user_type == 3: # 管事 # 使用select_related预取guanshi_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('guanshi_profile').first() + user_main = UserMain.query.filter(yonghuid=uid).select_related('guanshi_profile').first() if user_main.guanshi_profile: guanshi_profile = user_main.guanshi_profile user_info.update({ @@ -3944,7 +3946,7 @@ class AdckyhxqView(APIView): elif user_type == 4: # 商家 # 使用select_related预取shop_profile - user_main = UserMain.objects.filter(yonghuid=uid).select_related('shop_profile').first() + user_main = UserMain.query.filter(yonghuid=uid).select_related('shop_profile').first() if user_main.shop_profile: shop_profile = user_main.shop_profile user_info.update({ @@ -3964,7 +3966,7 @@ class AdckyhxqView(APIView): }) # 获取所有会员列表(用于前端下拉选择) - all_huiyuan = Huiyuan.objects.all().order_by('jiage') + all_huiyuan = Huiyuan.query.all().order_by('jiage') all_member_list = [ { 'id': huiyuan.huiyuan_id, @@ -4058,7 +4060,7 @@ class AdcjxgView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 查询用户主表 - user_main = UserMain.objects.filter(yonghuid=uid).first() + user_main = UserMain.query.filter(yonghuid=uid).first() if not user_main: return Response({ 'code': 404, @@ -4089,7 +4091,7 @@ class AdcjxgView(APIView): elif user_type == 2: # 打手 # 获取或创建打手扩展记录 - dashou_profile, created = UserDashou.objects.get_or_create( + dashou_profile, created = UserDashou.query.get_or_create( user=user_main, defaults={'nicheng': user_main.phone or f'打手{uid}'} ) @@ -4160,7 +4162,7 @@ class AdcjxgView(APIView): elif user_type == 3: # 管事 # 获取或创建管事扩展记录 - guanshi_profile, created = UserGuanshi.objects.get_or_create( + guanshi_profile, created = UserGuanshi.query.get_or_create( user=user_main, defaults={'yaoqingma': f'GS{uid[-4:]}'} ) @@ -4213,7 +4215,7 @@ class AdcjxgView(APIView): elif user_type == 4: # 商家 # 获取或创建商家扩展记录 - shop_profile, created = UserShangjia.objects.get_or_create( + shop_profile, created = UserShangjia.query.get_or_create( user=user_main, defaults={'nicheng': user_main.phone or f'商家{uid}'} ) @@ -4275,7 +4277,7 @@ class AdcjxgView(APIView): }, status=status.HTTP_400_BAD_REQUEST) # 检查会员是否存在 - huiyuan = Huiyuan.objects.filter(huiyuan_id=huiyuan_id).first() + huiyuan = Huiyuan.query.filter(huiyuan_id=huiyuan_id).first() if not huiyuan: return Response({ 'code': 404, @@ -4284,7 +4286,7 @@ class AdcjxgView(APIView): }, status=status.HTTP_404_NOT_FOUND) # 检查是否已购买该会员 - existing_record = Huiyuangoumai.objects.filter( + existing_record = Huiyuangoumai.query.filter( yonghu_id=uid, huiyuan_id=huiyuan_id ).first() @@ -4325,7 +4327,7 @@ class AdcjxgView(APIView): # 🔥 修改:新购买时只能增加天数 if days > 0: daoqi_time = timezone.now() + timedelta(days=days) - new_record = Huiyuangoumai.objects.create( + new_record = Huiyuangoumai.query.create( huiyuan_id=huiyuan_id, yonghu_id=uid, jieshao=huiyuan.jieshao, @@ -4362,10 +4364,10 @@ class AdcjxgView(APIView): # 如果有敏感字段被修改或会员操作,创建修改记录 if has_sensitive_change or 'huiyuan_id' in request.data: # 创建修改记录 - Xiugaijilu.objects.create(**modify_record_data) + Xiugaijilu.query.create(**modify_record_data) elif modify_record_data.get('qitashuoming', '').strip(): # 只有非敏感字段修改,也记录但qitashuoming不同 - Xiugaijilu.objects.create(**modify_record_data) + Xiugaijilu.query.create(**modify_record_data) return Response({ 'code': 0, @@ -4481,7 +4483,7 @@ class AddtxshView(APIView): query &= Q(yonghuid=search_uid) # 🔴【修改】排序改为按申请时间升序(最早申请排最前) - queryset = Tixianjilu.objects.filter(query).order_by('create_time') + queryset = Tixianjilu.query.filter(query).order_by('create_time') # 🔴【修改】分页逻辑:如果是搜索模式,不分页,返回所有匹配记录 if search_uid: @@ -4496,11 +4498,11 @@ class AddtxshView(APIView): # 获取状态统计(统计与搜索无关,依然按筛选条件统计) status_counts = { - 'awaiting': Tixianjilu.objects.filter( + 'awaiting': Tixianjilu.query.filter( zhuangtai=1, leixing=leixing ).count(), - 'processed': Tixianjilu.objects.filter( + 'processed': Tixianjilu.query.filter( zhuangtai__in=[2, 3], leixing=leixing ).count() @@ -4585,7 +4587,7 @@ class AdtixianqkView(APIView): # 获取提现记录 try: - withdrawal = Tixianjilu.objects.get(id=tixian_id) + withdrawal = Tixianjilu.query.get(id=tixian_id) except Tixianjilu.DoesNotExist: return Response({ 'code': 404, @@ -4617,7 +4619,7 @@ class AdtixianqkView(APIView): if result == 2: try: # 查询收支记录表,id=1的记录 - szjilu_record = Szjilu.objects.get(id=1) + szjilu_record = Szjilu.query.get(id=1) # 获取提现金额 tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段 @@ -4633,7 +4635,7 @@ class AdtixianqkView(APIView): except Szjilu.DoesNotExist: # 如果收支记录表不存在id=1的记录,创建一个 - szjilu_record = Szjilu.objects.create( + szjilu_record = Szjilu.query.create( id=1, zongsy=-tixian_jine if result == 2 else 0, zongzc=tixian_jine if result == 2 else 0, @@ -4678,7 +4680,7 @@ class DashouZiliaoHuoquView(APIView): def post(self, request): try: # 使用select_related高效查询,避免N+1问题 - dashou = UserDashou.objects.select_related('user').get(user=request.user) + dashou = UserDashou.query.select_related('user').get(user=request.user) # 构建返回数据 data = { @@ -4743,7 +4745,7 @@ class DashouGengxinView(APIView): # 获取打手扩展表 try: - dashou = UserDashou.objects.get(user=request.user) + dashou = UserDashou.query.get(user=request.user) except UserDashou.DoesNotExist: return Response({ 'code': 404, @@ -4884,7 +4886,7 @@ class ShangjiaZhuceView(APIView): # 3. 根据邀请码查找对应的管理员 try: # 从管理员表查找邀请码 - admin_profile = AdminProfile.objects.select_related('user').get(yaoqingma=yaoqingma) + admin_profile = AdminProfile.query.select_related('user').get(yaoqingma=yaoqingma) except AdminProfile.DoesNotExist: return Response({ 'code': 404, @@ -4910,12 +4912,12 @@ class ShangjiaZhuceView(APIView): shangjia_nicheng = boss_nickname # 检查昵称是否已存在(安全考虑) - if UserShangjia.objects.filter(nicheng=shangjia_nicheng).exists(): + if UserShangjia.query.filter(nicheng=shangjia_nicheng).exists(): # 如果存在,添加随机数确保唯一 shangjia_nicheng = f"{boss_nickname}{random.randint(1000, 9999)}" # 4.3 创建商家记录 - shop_profile = UserShangjia.objects.create( + shop_profile = UserShangjia.query.create( user=current_user, nicheng=shangjia_nicheng, zhuangtai=1, # 正常状态 @@ -5103,7 +5105,7 @@ class PaihangbangGuanliQueryView(APIView): ) # 5. 执行查询(按排名升序) - records = RankingRecord.objects.filter(query).order_by('paiming') + records = RankingRecord.query.filter(query).order_by('paiming') # 6. 格式化返回数据 data_list = [] @@ -5205,7 +5207,7 @@ class PaihangbangRiqiliebiaoView(APIView): # 2. 查询日榜日期(最近3个月内的) three_months_ago = timezone.now().date() - timedelta(days=90) - ribang_dates = RankingRecord.objects.filter( + ribang_dates = RankingRecord.query.filter( zhouqi=1, # 日榜 tongji_date__gte=three_months_ago # 只查询最近3个月 ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') @@ -5213,7 +5215,7 @@ class PaihangbangRiqiliebiaoView(APIView): # 3. 查询月榜日期(最近12个月内的) twelve_months_ago = timezone.now().date() - timedelta(days=365) - yuebang_dates = RankingRecord.objects.filter( + yuebang_dates = RankingRecord.query.filter( zhouqi=2, # 月榜 tongji_date__gte=twelve_months_ago # 只查询最近12个月 ).values_list('tongji_date', flat=True).distinct().order_by('-tongji_date') @@ -5329,12 +5331,12 @@ class WechatLoginAndDashouRegisterView(APIView): if created: # 新用户:创建老板扩展表 - UserBoss.objects.create(user=user_main, nickname='微信用户') + UserBoss.query.create(user=user_main, nickname='微信用户') # 4.2 验证邀请码对应的管事(注册逻辑) try: # 使用select_related获取管事及其关联的主表用户信息 - guanshi_profile = UserGuanshi.objects.select_related('user').get(yaoqingma=yaoqingma) + guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma) except UserGuanshi.DoesNotExist: # 邀请码无效,但用户已登录,可以返回登录成功但注册失败 # 这里我们继续执行,但只返回登录信息 @@ -5343,17 +5345,17 @@ class WechatLoginAndDashouRegisterView(APIView): # 验证管事状态 # 4.3 检查用户是否已是打手 - dashou_exists = UserDashou.objects.filter(user=user_main).exists() + dashou_exists = UserDashou.query.filter(user=user_main).exists() if dashou_exists: # 用户已是打手,直接返回信息 - dashou_profile = UserDashou.objects.get(user=user_main) + dashou_profile = UserDashou.query.get(user=user_main) return self.fanhuiDengluZhuceshuju(user_main, dashou_profile) # 4.4 核心:为用户创建所有身份(打手、商家、管事) guanshi_yonghuid = guanshi_profile.user.yonghuid # 创建打手扩展表 - dashou_profile = UserDashou.objects.create( + dashou_profile = UserDashou.query.create( user=user_main, nicheng='大神', chenghao='普通大神', @@ -5454,7 +5456,7 @@ class WechatLoginAndDashouRegisterView(APIView): user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): + if not UserMain.query.filter(yonghuid=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -5477,7 +5479,7 @@ class WechatLoginAndDashouRegisterView(APIView): 获取群配置信息 """ try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', @@ -5511,16 +5513,16 @@ class WechatLoginAndDashouRegisterView(APIView): # 获取老板昵称 boss_nickname = '微信用户' try: - boss_profile = UserBoss.objects.get(user=user_main) + boss_profile = UserBoss.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: pass # 检查各身份状态 - dashou_status = 1 if UserDashou.objects.filter(user=user_main).exists() else 0 - shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 0 - guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 0 + dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0 + shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0 + guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0 # 生成token refresh = RefreshToken.for_user(user_main) @@ -5566,7 +5568,7 @@ class WechatLoginAndDashouRegisterView(APIView): # 获取老板昵称 boss_nickname = '微信用户' try: - boss_profile = UserBoss.objects.get(user=user_main) + boss_profile = UserBoss.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: @@ -5574,8 +5576,8 @@ class WechatLoginAndDashouRegisterView(APIView): # 检查各身份状态(这里用户已是打手,所以都是1) dashou_status = 1 - shangjia_status = 1 if UserShangjia.objects.filter(user=user_main).exists() else 1 # 确保是1 - guanshi_status = 1 if UserGuanshi.objects.filter(user=user_main).exists() else 1 # 确保是1 + shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1 + guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1 # 生成token refresh = RefreshToken.for_user(user_main) @@ -5617,7 +5619,7 @@ class WechatLoginAndDashouRegisterView(APIView): # 获取老板昵称 boss_nickname = '微信用户' try: - boss_profile = UserBoss.objects.get(user=user_main) + boss_profile = UserBoss.query.get(user=user_main) if boss_profile.nickname: boss_nickname = boss_profile.nickname except UserBoss.DoesNotExist: @@ -5674,7 +5676,7 @@ class WechatLoginAndDashouRegisterView(APIView): # 查询并处理会员列表 (clumber) # 使用当前用户的yonghuid进行查询 huiyuan_goumai_list = [] - goumai_records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + goumai_records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.yonghuid) for record in goumai_records: # 调用方法检查是否到期,此方法内部会更新状态 shifou_daoqi = record.jiance_shifou_daoqi() @@ -6023,7 +6025,7 @@ class ChufaJiluHuoquView(APIView): """ try: # 使用单个查询获取所有统计信息 - stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( + stats = Chufajilu.query.filter(dashouid=yonghuid).aggregate( # 总记录数 zongshu=Count('id'), @@ -6085,7 +6087,7 @@ class ChufaJiluHuoquView(APIView): zhuangtai = request_data.get('zhuangtai', 0) # 🔴 构建查询条件 - 打手端根据dashouid查询 - queryset = Chufajilu.objects.filter(dashouid=yonghuid) + queryset = Chufajilu.query.filter(dashouid=yonghuid) if zhuangtai == 0: # 待处理:状态为0(待处罚)和3(申诉中) @@ -6156,7 +6158,7 @@ class ChufaJiluHuoquView(APIView): if dingdan_ids and shangjia_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid在商家ID列表中 zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=shangjia_ids) - zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') + zhengju_queryset = Chufatupian.query.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') for item in zhengju_queryset: key = f"{item['dingdan_id']}_{item['yonghuid']}" @@ -6170,7 +6172,7 @@ class ChufaJiluHuoquView(APIView): if dingdan_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前打手ID shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) - shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') + shensu_queryset = Chufatupian.query.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') for item in shensu_queryset: key = f"{item['dingdan_id']}_{item['yonghuid']}" @@ -6348,7 +6350,7 @@ class DashouShensuView(APIView): # 4. 检查是否已有申诉记录(防止重复申诉) if chufa_record.ssliyou or chufa_record.sqzhuangtai == 3: # 检查是否已经有申诉图片 - existing_shensu_tupian = Chufatupian.objects.filter( + existing_shensu_tupian = Chufatupian.query.filter( dingdan_id=chufa_record.dingdan_id, yonghuid=yonghuid ).exists() @@ -6370,7 +6372,7 @@ class DashouShensuView(APIView): if shensu_tupian_urls: for tupian_url in shensu_tupian_urls: if tupian_url: # 确保URL不为空 - chufa_tupian = Chufatupian.objects.create( + chufa_tupian = Chufatupian.query.create( dingdan_id=chufa_record.dingdan_id, yonghuid=yonghuid, tupian=tupian_url @@ -6464,7 +6466,7 @@ class ShangjiaChufaJiluHuoquView(APIView): """ try: # 🔴 查询条件:qingqiuid = 当前商家ID - stats = Chufajilu.objects.filter(qingqiuid=yonghuid).aggregate( + stats = Chufajilu.query.filter(qingqiuid=yonghuid).aggregate( zongshu=Count('id'), daichuli=Count( Case( @@ -6519,7 +6521,7 @@ class ShangjiaChufaJiluHuoquView(APIView): zhuangtai = request_data.get('zhuangtai', 0) # 🔴 查询条件:qingqiuid = 当前商家ID - queryset = Chufajilu.objects.filter(qingqiuid=yonghuid) + queryset = Chufajilu.query.filter(qingqiuid=yonghuid) if zhuangtai == 0: queryset = queryset.filter(Q(sqzhuangtai=0) | Q(sqzhuangtai=3)) @@ -6584,7 +6586,7 @@ class ShangjiaChufaJiluHuoquView(APIView): if dingdan_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid=当前商家ID zhengju_q = Q(dingdan_id__in=dingdan_ids, yonghuid=yonghuid) - zhengju_queryset = Chufatupian.objects.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') + zhengju_queryset = Chufatupian.query.filter(zhengju_q).values('dingdan_id', 'yonghuid', 'tupian') for item in zhengju_queryset: key = f"{item['dingdan_id']}_{item['yonghuid']}" @@ -6598,7 +6600,7 @@ class ShangjiaChufaJiluHuoquView(APIView): if dingdan_ids and dashou_ids: # 构建Q对象:dingdan_id在列表中 AND yonghuid在打手ID列表中 shensu_q = Q(dingdan_id__in=dingdan_ids, yonghuid__in=dashou_ids) - shensu_queryset = Chufatupian.objects.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') + shensu_queryset = Chufatupian.query.filter(shensu_q).values('dingdan_id', 'yonghuid', 'tupian') for item in shensu_queryset: key = f"{item['dingdan_id']}_{item['yonghuid']}" @@ -6717,7 +6719,7 @@ class KefuLoginView(APIView): # 3. 查询所有手机号匹配且类型为客服的用户 # 注意:UserMain 中需要有 user_type 字段,且值为 'kefu' 代表客服 - user_mains = UserMain.objects.filter( + user_mains = UserMain.query.filter( phone=phone, user_type='kefu' # 只查找客服账号 ).select_related('kefu_profile') # 预加载客服扩展表 @@ -6919,7 +6921,7 @@ class KefuGetOrderTypesView(APIView): # 4. 查询订单类型(商品类型) try: # 只查询需要的字段,减少数据传输 - types_qs = ShangpinLeixing.objects.all().only('id', 'jieshao', 'tupian_url') + types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url') type_list = [ { 'id': t.id, @@ -7027,7 +7029,7 @@ class KefuGetOrderListView(APIView): q_conditions &= Q(clkf__icontains=clkf) # ---------- 统计(仅按发单平台,非跨平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=1).aggregate( + stats = Dingdan.query.filter(fadan_pingtai=1).aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(zhuangtai=3)), refund_orders=Count('id', filter=Q(zhuangtai=5)), @@ -7036,20 +7038,20 @@ class KefuGetOrderListView(APIView): # ---------- 状态计数(所有状态都返回,非跨平台) ---------- status_counts = { - 'all': Dingdan.objects.filter(fadan_pingtai=1).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=1, zhuangtai=6).count(), + 'all': Dingdan.query.filter(fadan_pingtai=1).count(), + '1,7': Dingdan.query.filter(fadan_pingtai=1, zhuangtai__in=[1,7]).count(), + '2': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=2).count(), + '8': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=8).count(), + '3': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=3).count(), + '4': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=4).count(), + '5': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=5).count(), + '6': Dingdan.query.filter(fadan_pingtai=1, zhuangtai=6).count(), } # ---------- 分页查询 ---------- - total_count = Dingdan.objects.filter(q_conditions).count() + total_count = Dingdan.query.filter(q_conditions).count() - orders_qs = Dingdan.objects.filter(q_conditions).select_related( + orders_qs = Dingdan.query.filter(q_conditions).select_related( 'pingtai_kuozhan' ).only( 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', @@ -7061,7 +7063,7 @@ class KefuGetOrderListView(APIView): laoban_ids = [o.pingtai_kuozhan.laoban_id for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.laoban_id] nickname_map = {} if laoban_ids: - users = UserMain.objects.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only( + users = UserMain.query.filter(yonghuid__in=laoban_ids).select_related('boss_profile').only( 'yonghuid', 'boss_profile__nickname' ) for user in users: @@ -7191,12 +7193,12 @@ class KefuGetShangjiaOrderListView(APIView): q_conditions &= Q(clkf__icontains=clkf) if shangjia_nicheng: # 通过商家扩展表关联 UserShangjia 的昵称 - q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.objects.filter( + q_conditions &= Q(shangjia_kuozhan__shangjia_id__in=UserMain.query.filter( shop_profile__nicheng__icontains=shangjia_nicheng ).values_list('yonghuid', flat=True)) # ---------- 统计(仅按发单平台,非跨平台) ---------- - stats = Dingdan.objects.filter(fadan_pingtai=2).aggregate( + stats = Dingdan.query.filter(fadan_pingtai=2).aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(zhuangtai=3)), refund_orders=Count('id', filter=Q(zhuangtai=5)), @@ -7205,20 +7207,20 @@ class KefuGetShangjiaOrderListView(APIView): # ---------- 状态计数(所有状态都返回,非跨平台) ---------- status_counts = { - 'all': Dingdan.objects.filter(fadan_pingtai=2).count(), - '1,7': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai__in=[1,7]).count(), - '2': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=2).count(), - '8': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=8).count(), - '3': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=3).count(), - '4': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=4).count(), - '5': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=5).count(), - '6': Dingdan.objects.filter(fadan_pingtai=2, zhuangtai=6).count(), + 'all': Dingdan.query.filter(fadan_pingtai=2).count(), + '1,7': Dingdan.query.filter(fadan_pingtai=2, zhuangtai__in=[1,7]).count(), + '2': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=2).count(), + '8': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=8).count(), + '3': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=3).count(), + '4': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=4).count(), + '5': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=5).count(), + '6': Dingdan.query.filter(fadan_pingtai=2, zhuangtai=6).count(), } # ---------- 分页查询 ---------- - total_count = Dingdan.objects.filter(q_conditions).count() + total_count = Dingdan.query.filter(q_conditions).count() - orders_qs = Dingdan.objects.filter(q_conditions).select_related( + orders_qs = Dingdan.query.filter(q_conditions).select_related( 'shangjia_kuozhan' ).only( 'dingdan_id', 'jieshao', 'zhuangtai', 'leixing_id', 'fadan_pingtai', @@ -7230,7 +7232,7 @@ class KefuGetShangjiaOrderListView(APIView): shangjia_ids = [o.shangjia_kuozhan.shangjia_id for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.shangjia_id] nickname_map = {} if shangjia_ids: - users = UserMain.objects.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only( + users = UserMain.query.filter(yonghuid__in=shangjia_ids).select_related('shop_profile').only( 'yonghuid', 'shop_profile__nicheng' ) for user in users: @@ -7326,7 +7328,7 @@ class KefuChangeDashouView(APIView): # 3. 查询订单 try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) + order = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -7336,7 +7338,7 @@ class KefuChangeDashouView(APIView): # 5. 查询新打手 try: - new_dashou_user = UserMain.objects.get(yonghuid=new_dashou_id) + new_dashou_user = UserMain.query.get(yonghuid=new_dashou_id) except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -7363,7 +7365,7 @@ class KefuChangeDashouView(APIView): try: # 查询打手的会员购买记录 - huiyuan_goumai = Huiyuangoumai.objects.get( + huiyuan_goumai = Huiyuangoumai.query.get( huiyuan_id=huiyuan_id, yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID ) @@ -7398,7 +7400,7 @@ class KefuChangeDashouView(APIView): old_dashou_id = order.jiedan_dashou_id if old_dashou_id: try: - old_dashou_user = UserMain.objects.get(yonghuid=old_dashou_id) + old_dashou_user = UserMain.query.get(yonghuid=old_dashou_id) old_dashou_profile = old_dashou_user.dashou_profile old_dashou_profile.zhuangtai = 1 # 恢复空闲 old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑) @@ -7461,7 +7463,7 @@ class KefuRecoverOrderView(APIView): # 3. 查询订单 try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) + order = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -7537,7 +7539,7 @@ class KefuPlatformRefundView(APIView): # 3. 查询订单并加锁 try: with transaction.atomic(): - order = Dingdan.objects.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) + order = Dingdan.query.select_related('pingtai_kuozhan').select_for_update().get(dingdan_id=dingdan_id) # 4. 验证平台订单 if order.fadan_pingtai != 1: @@ -7574,7 +7576,7 @@ class KefuPlatformRefundView(APIView): jiedan_dashou_id = order.jiedan_dashou_id if jiedan_dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 # 打手状态设为空闲 @@ -7629,7 +7631,7 @@ class KefuPlatformRefundView(APIView): try: - refund_record, created = Tuikuanjilu.objects.get_or_create( + refund_record, created = Tuikuanjilu.query.get_or_create( dingdan_id=dingdan_id, defaults={ 'dashouid': jiedan_dashou_id or '', @@ -7839,7 +7841,7 @@ class KefuRejectRefundView(APIView): # 7. 给打手结算(返还分成) if jiedan_dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile # 增加打手相关统计数据 dashou_profile.chengjiaozongliang += 1 @@ -7859,7 +7861,7 @@ class KefuRejectRefundView(APIView): shangjia_kuozhan = order.shangjia_kuozhan shangjia_id = shangjia_kuozhan.shangjia_id if shangjia_id: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_user = UserMain.query.get(yonghuid=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao += 1 shangjia_profile.save() @@ -7876,7 +7878,7 @@ class KefuRejectRefundView(APIView): # 9. 更新退款记录表 try: - refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + refund_record = Tuikuanjilu.query.get(dingdan_id=dingdan_id) refund_record.sqzhuangtai = 2 # 2表示拒绝 refund_record.chuliid = phone refund_record.bhliyou = jujue_liyou # 记录驳回理由(假设模型有此字段) @@ -7884,7 +7886,7 @@ class KefuRejectRefundView(APIView): logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}") except Tuikuanjilu.DoesNotExist: # 如果退款记录不存在,可以创建一条记录(可选) - Tuikuanjilu.objects.create( + Tuikuanjilu.query.create( dingdan_id=dingdan_id, dashouid=jiedan_dashou_id or '', qingqiuid='', @@ -7980,7 +7982,7 @@ class KefuMerchantRefundView(APIView): # 查询订单,预加载商家扩展表 try: - order = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) + order = Dingdan.query.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -8016,7 +8018,7 @@ class KefuMerchantRefundView(APIView): # 更新打手 if jiedan_dashou_id: try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 @@ -8026,7 +8028,7 @@ class KefuMerchantRefundView(APIView): # 更新商家 try: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_id) + shangjia_user = UserMain.query.get(yonghuid=shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.tuikuan += 1 shangjia_profile.yue += jine @@ -8036,7 +8038,7 @@ class KefuMerchantRefundView(APIView): # 更新退款记录表 try: - refund_record = Tuikuanjilu.objects.get(dingdan_id=dingdan_id) + refund_record = Tuikuanjilu.query.get(dingdan_id=dingdan_id) refund_record.sqzhuangtai = 1 refund_record.chuliid = phone refund_record.save() @@ -8121,7 +8123,7 @@ class KefuRejectSettlementView(APIView): # 3. 查询订单,预加载平台扩展表 try: - order = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) + order = Dingdan.query.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -8153,7 +8155,7 @@ class KefuRejectSettlementView(APIView): # 5.3 更新打手扩展表 try: - dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id) + dashou_user = UserMain.query.get(yonghuid=jiedan_dashou_id) dashou_profile = dashou_user.dashou_profile dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.tuikuanliang += 1 # 退款订单总量+1 @@ -8227,7 +8229,7 @@ class KefuTransferHallView(APIView): # 3. 查询订单 try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) + order = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -8237,7 +8239,7 @@ class KefuTransferHallView(APIView): if order.jiedan_dashou_id: - UserDashou.objects.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1) + UserDashou.query.filter(user__yonghuid=order.jiedan_dashou_id).update(zhuangtai=1) else: pass @@ -8295,7 +8297,7 @@ class KefuCancelDesignationView(APIView): # 3. 查询订单 try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) + order = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -8379,7 +8381,7 @@ class KefuGetDashouListView(APIView): # 3. 构建查询条件(只查询打手) # 通过 UserDashou 扩展表关联 UserMain - queryset = UserDashou.objects.select_related('user').all() + queryset = UserDashou.query.select_related('user').all() if keyword: # 搜索用户ID或打手昵称 @@ -8468,7 +8470,7 @@ class KefuGetDashouDetailView(APIView): # 3. 查询打手主表和扩展表 try: # 使用 select_related 预加载 dashou_profile,减少数据库查询 - user_main = UserMain.objects.select_related('dashou_profile').get( + user_main = UserMain.query.select_related('dashou_profile').get( yonghuid=uid ) except UserMain.DoesNotExist: @@ -8544,7 +8546,7 @@ class KefuGetDashouDetailView(APIView): } # 5. 查询打手会员信息(仅未过期) - member_records = Huiyuangoumai.objects.filter( + member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-create_time') @@ -8560,7 +8562,7 @@ class KefuGetDashouDetailView(APIView): if not is_expired: # 获取会员详情 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=record.huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id) huiyuan_name = huiyuan.jieshao except Huiyuan.DoesNotExist: huiyuan_name = '未知会员' @@ -8614,7 +8616,7 @@ class KefuUpdateDashouView(APIView): # 3. 查询打手是否存在 try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id, user_type='dashou') + dashou_user = UserMain.query.get(yonghuid=dashou_id, user_type='dashou') dashou_profile = dashou_user.dashou_profile except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) @@ -8645,7 +8647,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.save() after_yue = dashou_profile.yue # 记录日志 - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, # 2=打手 @@ -8661,7 +8663,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yue += amount dashou_profile.save() after_yue = dashou_profile.yue - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8680,7 +8682,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin -= amount dashou_profile.save() after_yajin = dashou_profile.yajin - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8696,7 +8698,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.yajin += amount dashou_profile.save() after_yajin = dashou_profile.yajin - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8715,7 +8717,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen -= jifen dashou_profile.save() after_jifen = dashou_profile.jifen - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8731,7 +8733,7 @@ class KefuUpdateDashouView(APIView): dashou_profile.jifen += jifen dashou_profile.save() after_jifen = dashou_profile.jifen - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8749,7 +8751,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8765,7 +8767,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8781,7 +8783,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8797,7 +8799,7 @@ class KefuUpdateDashouView(APIView): return Response({'code': 400, 'msg': '缺少会员ID'}) days = int(value) if value else 30 # 默认加30天 try: - huiyuan = Huiyuan.objects.get(huiyuan_id=huiyuan_id) + huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员类型不存在'}) # 获取或创建购买记录 @@ -8817,7 +8819,7 @@ class KefuUpdateDashouView(APIView): record.huiyuan_zhuangtai = 1 record.save() # 记录修改日志 - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8833,12 +8835,12 @@ class KefuUpdateDashouView(APIView): if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) try: - record = Huiyuangoumai.objects.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) + record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id) except Huiyuangoumai.DoesNotExist: return Response({'code': 404, 'msg': '该打手未购买此会员'}) # 删除会员记录(或设置为过期,根据业务要求) record.delete() - Xiugaijilu.objects.create( + Xiugaijilu.query.create( yonghuid=dashou_id, xiugaiid=kefu.user.phone, leixing=2, @@ -8923,13 +8925,13 @@ class KefuPunishmentListView(APIView): # 3. 获取统计信息(不分页) stats = { - 'zongshu': Chufajilu.objects.count(), - 'daichuli': Chufajilu.objects.filter(sqzhuangtai__in=[0, 3]).count(), - 'yichuli': Chufajilu.objects.filter(sqzhuangtai__in=[1, 2]).count(), + 'zongshu': Chufajilu.query.count(), + 'daichuli': Chufajilu.query.filter(sqzhuangtai__in=[0, 3]).count(), + 'yichuli': Chufajilu.query.filter(sqzhuangtai__in=[1, 2]).count(), } # 4. 构建查询条件 - queryset = Chufajilu.objects.all() + queryset = Chufajilu.query.all() if status_list and isinstance(status_list, list): queryset = queryset.filter(sqzhuangtai__in=status_list) @@ -9024,7 +9026,7 @@ class KefuGetOrderDetailView(APIView): # 3. 查询订单 try: #p(f"开始查询订单: {dingdan_id}") - dingdan_obj = Dingdan.objects.select_related( + dingdan_obj = Dingdan.query.select_related( 'pingtai_kuozhan', 'shangjia_kuozhan' ).get(dingdan_id=dingdan_id) #p("订单查询成功") @@ -9097,7 +9099,7 @@ class KefuGetOrderDetailView(APIView): # 6. 打手图片 try: - dashou_images = Dashoutupian.objects.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True) + dashou_images = Dashoutupian.query.filter(dingdan_id=dingdan_id).values_list('tupian', flat=True) response_data['dashou_images'] = [img for img in dashou_images if img] #p(f"打手图片数量: {len(response_data['dashou_images'])}") except Exception as e: @@ -9106,7 +9108,7 @@ class KefuGetOrderDetailView(APIView): # 7. 退款理由 try: - tuikuan_record = Tuikuanjilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() + tuikuan_record = Tuikuanjilu.query.filter(dingdan_id=dingdan_id).order_by('-create_time').first() response_data['tuikuan_liyou'] = tuikuan_record.liyou if tuikuan_record and tuikuan_record.liyou else '' #p("退款理由查询完成") except Exception as e: @@ -9174,7 +9176,7 @@ class KefuPunishmentDetailView(APIView): # 查询处罚记录(按创建时间倒序取最新一条) try: - record = Chufajilu.objects.filter(dingdan_id=dingdan_id).order_by('-create_time').first() + record = Chufajilu.query.filter(dingdan_id=dingdan_id).order_by('-create_time').first() if not record: return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND) except Exception as e: @@ -9186,11 +9188,11 @@ class KefuPunishmentDetailView(APIView): shensu_images = [] try: # 商家证据图片:上传者为请求人 (qingqiuid) - zhengju_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid) + zhengju_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, yonghuid=record.qingqiuid) zhengju_images = [item.tupian for item in zhengju_qs if item.tupian] # 打手申诉图片:上传者为打手 (dashouid) - shensu_qs = Chufatupian.objects.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid) + shensu_qs = Chufatupian.query.filter(dingdan_id=dingdan_id, yonghuid=record.dashouid) shensu_images = [item.tupian for item in shensu_qs if item.tupian] except Exception as e: logger.error(f"查询图片失败: {e}") @@ -9260,12 +9262,12 @@ class KefuPunishmentActionView(APIView): # 查询待处理的处罚记录(状态0或3) try: - record = Chufajilu.objects.filter( + record = Chufajilu.query.filter( dingdan_id=dingdan_id, sqzhuangtai__in=[0, 3] ).order_by('-create_time').first() if not record: - exists = Chufajilu.objects.filter(dingdan_id=dingdan_id).exists() + exists = Chufajilu.query.filter(dingdan_id=dingdan_id).exists() if exists: return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST) else: @@ -9276,7 +9278,7 @@ class KefuPunishmentActionView(APIView): with transaction.atomic(): # 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回 - duplicate_records = Chufajilu.objects.filter( + duplicate_records = Chufajilu.query.filter( dingdan_id=dingdan_id, sqzhuangtai__in=[0, 3] ).exclude(id=record.id) @@ -9291,7 +9293,7 @@ class KefuPunishmentActionView(APIView): # 可选验证打手存在 if record.dashouid: try: - dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) + dashou = UserDashou.query.get(user__yonghuid=record.dashouid) except UserDashou.DoesNotExist: return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST) @@ -9301,7 +9303,7 @@ class KefuPunishmentActionView(APIView): # 更新商家订单扩展表 try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.sqzhuangtai = 1 dingdan.shangjia_kuozhan.save() @@ -9312,7 +9314,7 @@ class KefuPunishmentActionView(APIView): # 返还积分 if record.jifen > 0 and record.dashouid: try: - dashou = UserDashou.objects.get(user__yonghuid=record.dashouid) + dashou = UserDashou.query.get(user__yonghuid=record.dashouid) if dashou.jifen <= 1000: # 安全限制 dashou.jifen += record.jifen dashou.save() @@ -9328,7 +9330,7 @@ class KefuPunishmentActionView(APIView): record.save() try: - dingdan = Dingdan.objects.get(dingdan_id=dingdan_id) + dingdan = Dingdan.query.get(dingdan_id=dingdan_id) if hasattr(dingdan, 'shangjia_kuozhan'): dingdan.shangjia_kuozhan.sqzhuangtai = 2 dingdan.shangjia_kuozhan.bhliyou = reject_reason @@ -9407,7 +9409,7 @@ class KefuForceCompleteView(APIView): # 5. 给打手结算 try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou_profile = dashou_user.dashou_profile # 原子更新(避免并发重复加) dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1 @@ -9428,7 +9430,7 @@ class KefuForceCompleteView(APIView): # 通过 DingdanShangjia 获取商家ID shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan' if shangjia_ext and shangjia_ext.shangjia_id: - shangjia_user = UserMain.objects.get(yonghuid=shangjia_ext.shangjia_id) + shangjia_user = UserMain.query.get(yonghuid=shangjia_ext.shangjia_id) shangjia_profile = shangjia_user.shop_profile shangjia_profile.chengjiao = F('chengjiao') + 1 shangjia_profile.save(update_fields=['chengjiao']) @@ -9588,7 +9590,7 @@ class KefuWithdrawListView(APIView): def _shenhe_jilu_field_available(self): """探测 shenhe_jilu_id 字段是否已在数据库中就绪""" try: - Tixianjilu.objects.values_list('shenhe_jilu_id', flat=True).first() + Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first() return True except DatabaseError as e: kefu_txsh_list_logger.error( @@ -9602,7 +9604,7 @@ class KefuWithdrawListView(APIView): fields = _TIXIAN_LIST_BASE_FIELDS + ( _TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else () ) - return Tixianjilu.objects.filter(q).only(*fields) + return Tixianjilu.query.filter(q).only(*fields) def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok): """无关联审核或审核表未标记自动 → 默认手动(1)""" @@ -9865,13 +9867,13 @@ class KefuWithdrawDetailView(APIView): # 查询提现记录 try: - record = Tixianjilu.objects.get(id=tixian_id) + record = Tixianjilu.query.get(id=tixian_id) except Tixianjilu.DoesNotExist: return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND) # 查询用户主表 try: - user = UserMain.objects.get(yonghuid=record.yonghuid) + user = UserMain.query.get(yonghuid=record.yonghuid) except UserMain.DoesNotExist: user = None @@ -9898,7 +9900,7 @@ class KefuWithdrawDetailView(APIView): if user: if record.leixing == 1: # 打手 try: - dashou = UserDashou.objects.get(user=user) + dashou = UserDashou.query.get(user=user) # 计算成交率和退款率 total = dashou.jiedanzongliang or 0 completed = dashou.chengjiaozongliang or 0 @@ -9923,7 +9925,7 @@ class KefuWithdrawDetailView(APIView): data['dashouInfo'] = None elif record.leixing == 2: # 管事 try: - guanshi = UserGuanshi.objects.get(user=user) + guanshi = UserGuanshi.query.get(user=user) data['guanshiInfo'] = { 'zhuangtai': guanshi.zhuangtai, # 账号状态 'yue': float(guanshi.yue) if guanshi.yue else 0.00, @@ -10097,7 +10099,7 @@ class KefuWithdrawActionView(APIView): raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程') try: - user = UserMain.objects.get(yonghuid=tixian.yonghuid) + user = UserMain.query.get(yonghuid=tixian.yonghuid) except UserMain.DoesNotExist: raise ValueError(f'用户{tixian.yonghuid}不存在') @@ -10105,7 +10107,7 @@ class KefuWithdrawActionView(APIView): tixian.zhuangtai = 2 try: update_daily_payout(tixian.jine) - szjilu, _ = Szjilu.objects.get_or_create(id=1) + szjilu, _ = Szjilu.query.get_or_create(id=1) szjilu.zongsy -= tixian.jine szjilu.zongzc += tixian.jine szjilu.jrzc += tixian.jine @@ -10233,7 +10235,7 @@ class KefuPunishView(APIView): # 3. 查询订单 try: - order = Dingdan.objects.get(dingdan_id=dingdan_id) + order = Dingdan.query.get(dingdan_id=dingdan_id) except Dingdan.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -10244,7 +10246,7 @@ class KefuPunishView(APIView): # 5. 查询打手 try: - dashou_user = UserMain.objects.get(yonghuid=dashou_id) + dashou_user = UserMain.query.get(yonghuid=dashou_id) dashou = dashou_user.dashou_profile except UserMain.DoesNotExist: return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND) @@ -10255,7 +10257,7 @@ class KefuPunishView(APIView): with transaction.atomic(): # 创建处罚记录(积分扣除5分) jifen = 5 - chufa = Chufajilu.objects.create( + chufa = Chufajilu.query.create( dashouid=dashou_id, qingqiuid=phone, # 客服手机号作为请求人 cfliyou=reason or '', @@ -10317,7 +10319,7 @@ class AdKftjView(APIView): return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 3. 检查手机号是否已被使用 - if UserMain.objects.filter(phone=phone).exists(): + if UserMain.query.filter(phone=phone).exists(): return Response({'code': 400, 'message': '该手机号已被注册'}) # 4. 生成唯一 yonghuid @@ -10326,13 +10328,13 @@ class AdKftjView(APIView): timestamp = str(int(time.time()))[-5:] rand = str(random.randint(0, 99)).zfill(2) uid = timestamp + rand - if len(uid) == 7 and not UserMain.objects.filter(yonghuid=uid).exists(): + if len(uid) == 7 and not UserMain.query.filter(yonghuid=uid).exists(): return uid raise Exception('无法生成唯一用户ID') with transaction.atomic(): # 创建主表用户 - user = UserMain.objects.create( + user = UserMain.query.create( yonghuid=generate_yonghuid(), phone=phone, password=password, @@ -10340,7 +10342,7 @@ class AdKftjView(APIView): openid=f'kefu_{phone}' # 临时openid ) # 创建客服扩展表 - kefu = UserKefu.objects.create( + kefu = UserKefu.query.create( user=user, nicheng=nicheng or f'客服{user.yonghuid}', erjimima=erjimima, # 建议实际项目中加密存储 @@ -10395,7 +10397,7 @@ class AdKfglView(APIView): return Response({'code': 401, 'message': '身份验证失败'}, status=status.HTTP_401_UNAUTHORIZED) # 3. 查询所有客服(通过扩展表关联主表) - queryset = UserKefu.objects.select_related('user').all() + queryset = UserKefu.query.select_related('user').all() if keyword: queryset = queryset.filter( @@ -10464,7 +10466,7 @@ class AdKfxgView(APIView): # 查询客服 try: - user = UserMain.objects.get(phone=uid) + user = UserMain.query.get(phone=uid) kefu = user.kefu_profile except (UserMain.DoesNotExist, UserKefu.DoesNotExist): return Response({'code': 404, 'message': '客服不存在'}) @@ -10474,7 +10476,7 @@ class AdKfxgView(APIView): # 更新主表字段 if 'phone' in request.data: new_phone = request.data.get('phone').strip() - if new_phone and UserMain.objects.filter(phone=new_phone).exclude(phone=uid).exists(): + if new_phone and UserMain.query.filter(phone=new_phone).exclude(phone=uid).exists(): return Response({'code': 400, 'message': '手机号已被使用'}) user.phone = new_phone if 'password' in request.data: @@ -10548,13 +10550,13 @@ class WechatLoginAndGuanshiRegisterView(APIView): user_main.save() if created: - UserBoss.objects.create(user=user_main, nickname='微信用户') + UserBoss.query.create(user=user_main, nickname='微信用户') if hasattr(user_main, 'guanshi_profile'): return self._fanhui_xianyou_guanshi_info(user_main) try: - zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) + zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND) @@ -10566,13 +10568,13 @@ class WechatLoginAndGuanshiRegisterView(APIView): zuzhang_yonghuid = zuzhang.user.yonghuid guanshi_yaoqingma = CreateInvitationCode(str(user_main.yonghuid)) - guanshi_profile = UserGuanshi.objects.create( + guanshi_profile = UserGuanshi.query.create( user=user_main, yaoqingma=guanshi_yaoqingma, yaoqingren=zuzhang_yonghuid, ) - UserZuzhang.objects.filter(pk=zuzhang.pk).update( + UserZuzhang.query.filter(pk=zuzhang.pk).update( yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 ) @@ -10588,7 +10590,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): if not hasattr(user_main, 'dashou_profile'): - dashou_profile = UserDashou.objects.create( + dashou_profile = UserDashou.query.create( user=user_main, nicheng='大神', chenghao='普通大神', @@ -10642,7 +10644,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): random_part = str(random.randint(0, 99)).zfill(2) user_id = timestamp_part + random_part if len(user_id) == 7 and user_id.isdigit(): - if not UserMain.objects.filter(yonghuid=user_id).exists(): + if not UserMain.query.filter(yonghuid=user_id).exists(): return user_id raise Exception('生成用户ID失败') @@ -10657,7 +10659,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): def _huoqu_qun_peizhi(self): try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', 'dashouqunid': '', @@ -10691,7 +10693,7 @@ class WechatLoginAndGuanshiRegisterView(APIView): } huiyuan_list = [] try: - records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.yonghuid) for record in records: shifou_daoqi = record.jiance_shifou_daoqi() huiyuan_list.append({ @@ -10821,7 +10823,7 @@ class GuanshiRegisterView(APIView): # 3. 验证邀请码对应的组长 try: - zuzhang = UserZuzhang.objects.select_related('user').get(yaoqingma=yaoqingma) + zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND) @@ -10836,14 +10838,14 @@ class GuanshiRegisterView(APIView): with transaction.atomic(): # 创建管事扩展表 guanshi_yaoqingma = CreateInvitationCode(str(current_user.yonghuid)) - guanshi_profile = UserGuanshi.objects.create( + guanshi_profile = UserGuanshi.query.create( user=current_user, yaoqingma=guanshi_yaoqingma, yaoqingren=zuzhang_yonghuid, ) # 更新组长的邀请总数 - UserZuzhang.objects.filter(pk=zuzhang.pk).update( + UserZuzhang.query.filter(pk=zuzhang.pk).update( yaoqing_zongshu=models.F('yaoqing_zongshu') + 1 ) @@ -10859,7 +10861,7 @@ class GuanshiRegisterView(APIView): # 检查用户是否已是打手,如果不是则创建 if not hasattr(current_user, 'dashou_profile'): - dashou_profile = UserDashou.objects.create( + dashou_profile = UserDashou.query.create( user=current_user, nicheng='大手子', chenghao='普通大手', @@ -10884,7 +10886,7 @@ class GuanshiRegisterView(APIView): def _huoqu_qun_peizhi(self): """获取群配置信息""" try: - qun_configs = Qunpeizhi.objects.filter(id__in=[1, 2]) + qun_configs = Qunpeizhi.query.filter(id__in=[1, 2]) result = { 'dashouqun': '', 'dashouqunid': '', @@ -10921,7 +10923,7 @@ class GuanshiRegisterView(APIView): # 会员列表 huiyuan_list = [] try: - records = Huiyuangoumai.objects.filter(yonghu_id=dashou_profile.user.yonghuid) + records = Huiyuangoumai.query.filter(yonghu_id=dashou_profile.user.yonghuid) for record in records: shifou_daoqi = record.jiance_shifou_daoqi() huiyuan_list.append({ @@ -11073,7 +11075,7 @@ class ZuzhangZhuceView(APIView): # 检查是否已是组长 try: - zuzhang = UserZuzhang.objects.select_related('user').get(user=user) + zuzhang = UserZuzhang.query.select_related('user').get(user=user) # 已注册,直接返回数据 data = self._build_response_data(zuzhang, user) return Response({'code':200, 'msg': '已注册', 'data': data}) @@ -11090,7 +11092,7 @@ class ZuzhangZhuceView(APIView): # 创建组长扩展记录 try: with transaction.atomic(): - zuzhang = UserZuzhang.objects.create( + zuzhang = UserZuzhang.query.create( user=user, yaoqingma=yaoqingma, fenyong_zonge=0.00, @@ -11108,7 +11110,7 @@ class ZuzhangZhuceView(APIView): except IntegrityError: # 极低概率重复,重试一次 yaoqingma = self._generate_unique_yaoqingma(user.yonghuid, retry=True) - zuzhang = UserZuzhang.objects.create( + zuzhang = UserZuzhang.query.create( user=user, yaoqingma=yaoqingma, # 其他字段默认同上 @@ -11136,7 +11138,7 @@ class ZuzhangZhuceView(APIView): base = f"Z{timestamp}{uid_part}{rand_str}" # 确保唯一性(若已存在则追加随机数,最多尝试3次) for _ in range(3): - if not UserZuzhang.objects.filter(yaoqingma=base).exists(): + if not UserZuzhang.query.filter(yaoqingma=base).exists(): return base base = base + random.choice(string.digits) # 极端情况,再附加时间戳毫秒 @@ -11146,7 +11148,7 @@ class ZuzhangZhuceView(APIView): def _build_response_data(self, zuzhang, user): """组装前端所需数据""" # 计算邀请的管事数量(通过UserGuanshi表的yaoqingren字段关联到user的yonghuid) - guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() + guanshi_count = UserGuanshi.query.filter(yaoqingren=user.yonghuid).count() return { 'ketixian': str(zuzhang.ketixian_jine), # 可提现余额 'guanshiCount': guanshi_count, # 邀请管事数 @@ -11168,11 +11170,11 @@ class ZuzhangXinxiView(APIView): def post(self, request): user = request.user try: - zuzhang = UserZuzhang.objects.get(user=user) + zuzhang = UserZuzhang.query.get(user=user) except UserZuzhang.DoesNotExist: return Response({'code': 404, 'msg': '用户不是组长'}, status=404) - guanshi_count = UserGuanshi.objects.filter(yaoqingren=user.yonghuid).count() + guanshi_count = UserGuanshi.query.filter(yaoqingren=user.yonghuid).count() data = { 'ketixian': str(zuzhang.ketixian_jine), 'guanshiCount': guanshi_count, @@ -11227,7 +11229,7 @@ class DashouJianquanView(APIView): # ========== 4. 条件二:存在未过期会员 ========== # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 - huiyuan_list = Huiyuangoumai.objects.filter( + huiyuan_list = Huiyuangoumai.query.filter( yonghu_id=user.yonghuid, huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') @@ -11242,7 +11244,7 @@ class DashouJianquanView(APIView): # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 - has_valid_order = Dingdan.objects.filter( + has_valid_order = Dingdan.query.filter( jiedan_dashou_id=user.yonghuid, zhuangtai__in=[2, 8] ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 @@ -11289,7 +11291,7 @@ class LaobanJianquanView(APIView): # ========== 2. 高效查询平台订单扩展表 ========== # 利用 DingdanPingtai.laoban_id 索引 + Dingdan.zhuangtai 索引 # 只查 1 条,用 exists() 实现最高性能 - has_valid_order = DingdanPingtai.objects.filter( + has_valid_order = DingdanPingtai.query.filter( laoban_id=user.yonghuid ).filter( dingdan__zhuangtai__in=[1, 2, 3, 4, 5, 6, 7, 8] @@ -11328,7 +11330,7 @@ class FaKuanJiFenTongJiView(APIView): yonghuid = user_main.yonghuid # ================= 罚款统计 ================= - fadan_stats = Fadan.objects.filter(beichufa_id=yonghuid).aggregate( + fadan_stats = Fadan.query.filter(beichufa_id=yonghuid).aggregate( total=Count('id'), daijiaona=Count('id', filter=Q(zhuangtai=1)), shensuzhong=Count('id', filter=Q(zhuangtai=3)), @@ -11337,7 +11339,7 @@ class FaKuanJiFenTongJiView(APIView): ) # ================= 积分处罚统计 ================= - jifen_stats = Chufajilu.objects.filter(dashouid=yonghuid).aggregate( + jifen_stats = Chufajilu.query.filter(dashouid=yonghuid).aggregate( total=Count('id'), daichuli=Count('id', filter=Q(sqzhuangtai=0) | Q(sqzhuangtai=3)), yichuli=Count('id', filter=Q(sqzhuangtai=1) | Q(sqzhuangtai=2)), @@ -11414,7 +11416,7 @@ class DaShouFaKuanLieBiaoView(APIView): sousuo_dingdan_id = request.data.get('sousuo_dingdan_id', '').strip() # 基础查询:当前用户的罚单 - qs = Fadan.objects.filter(beichufa_id=yonghuid) + qs = Fadan.query.filter(beichufa_id=yonghuid) # 状态筛选 if zhuangtai is not None and int(zhuangtai) in [1, 2, 3, 4]: @@ -11444,7 +11446,7 @@ class DaShouFaKuanLieBiaoView(APIView): zhengju_map = {} # key: fadan_id, value: [url, ...] shensu_map = {} if fadan_ids: - tupians = FadanShensuTupian.objects.filter( + tupians = FadanShensuTupian.query.filter( fadan_id__in=fadan_ids ).values('fadan_id', 'tupian_url', 'yongtu') for t in tupians: @@ -11522,7 +11524,7 @@ class FaKuanShenSuView(APIView): # 查询罚单,确保属于当前打手 try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=yonghuid) + fadan = Fadan.query.get(id=fadan_id, beichufa_id=yonghuid) except Fadan.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在或无权操作'}, status=404) @@ -11538,11 +11540,11 @@ class FaKuanShenSuView(APIView): fadan.save() # 删除旧的申诉图片(yongtu=2),仅删除打手自己上传的申诉图 - FadanShensuTupian.objects.filter(fadan=fadan, yongtu=2).delete() + FadanShensuTupian.query.filter(fadan=fadan, yongtu=2).delete() # 插入新上传的申诉图片 for url in tupian_urls: - FadanShensuTupian.objects.create( + FadanShensuTupian.query.create( fadan=fadan, beichufa_id=yonghuid, tupian_url=url, @@ -11580,7 +11582,7 @@ class FaKuanPayView(APIView): return Response({'code': 400, 'message': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) try: - fadan = Fadan.objects.get(id=fadan_id, beichufa_id=request.user.yonghuid) + fadan = Fadan.query.get(id=fadan_id, beichufa_id=request.user.yonghuid) except Fadan.DoesNotExist: return Response({'code': 404, 'message': '罚单不存在或不属于您'}, status=status.HTTP_404_NOT_FOUND) @@ -11594,7 +11596,7 @@ class FaKuanPayView(APIView): random_num = random.randint(0, 99) dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}" - chongzhi_jilu = Czjilu.objects.create( + chongzhi_jilu = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, yonghuid=request.user.yonghuid, @@ -11777,7 +11779,7 @@ class FaKuanHuitiaoView(View): # 4. 查询订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no) + order = Czjilu.query.get(dingdan_id=out_trade_no) logger.info(f"找到订单: {order.dingdan_id}, 状态: {order.zhuangtai}, 金额: {order.jine}") except Czjilu.DoesNotExist: logger.error(f"订单不存在: {out_trade_no}") @@ -11806,7 +11808,7 @@ class FaKuanHuitiaoView(View): # 8. 更新罚单状态为已缴纳(2) try: # 根据订单中的用户ID和金额匹配待缴纳的罚单 - fadan = Fadan.objects.filter( + fadan = Fadan.query.filter( beichufa_id=order.yonghuid, fakuanjine=order.jine, zhuangtai__in=[1, 3] @@ -11834,7 +11836,7 @@ class FaKuanHuitiaoView(View): update_daily_income(jine_decimal) # 查询或创建ID=1的收支记录 - szjilu, created = Szjilu.objects.get_or_create( + szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ 'zongsy': Decimal('0.00'), @@ -11928,7 +11930,7 @@ class FaKuanPayPollView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) if order.zhuangtai == 3: return Response({'code': 200, 'message': '支付成功'}) else: @@ -11951,7 +11953,7 @@ class FaKuanPayFailView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid) if order.zhuangtai == 9: # 未支付 order.delete() logger.info(f"用户{request.user.yonghuid}删除未支付订单: {dingdanid}") @@ -12000,7 +12002,7 @@ class KaohePayView(APIView): return Response({'code': 400, 'message': '标签ID和游戏昵称不能为空'}) # 1. 检查是否有未支付的考核订单 - """unpaid_order = Czjilu.objects.filter( + """unpaid_order = Czjilu.query.filter( yonghuid=user.yonghuid, leixing=5, zhuangtai=9 @@ -12011,19 +12013,19 @@ class KaohePayView(APIView): # 2. 验证标签 try: - chenghao = Chenghao.objects.get(id=tag_id) + chenghao = Chenghao.query.get(id=tag_id) logger.info(f"标签验证通过: {chenghao.mingcheng}") except Chenghao.DoesNotExist: logger.error(f"标签不存在: tag_id={tag_id}") return Response({'code': 404, 'message': '标签不存在'}) # 3. 检查用户是否已拥有该标签 - if YonghuChenghao.objects.filter(yonghu=user, chenghao=chenghao).exists(): + if YonghuChenghao.query.filter(yonghu=user, chenghao=chenghao).exists(): logger.warning(f"用户 {user.yonghuid} 已拥有标签 {chenghao.mingcheng}") return Response({'code': 400, 'message': '您已拥有该标签,无需重复考核'}) # 4. 检查是否有未完成的申请 - if ShenheJilu.objects.filter( + if ShenheJilu.query.filter( shenqingren_id=user.yonghuid, chenghao=chenghao, zhuangtai__in=[0, 3] # 审核中、待审核 @@ -12033,7 +12035,7 @@ class KaohePayView(APIView): # 5. 计算考核次数和费用 if reapply and jilu_id: - last = ShenheJilu.objects.filter( + last = ShenheJilu.query.filter( jilu_id=jilu_id, shenqingren_id=user.yonghuid, zhuangtai=2 @@ -12044,7 +12046,7 @@ class KaohePayView(APIView): new_cishu = last.cishu + 1 logger.info(f"再次申请,原记录ID: {jilu_id},新次数: {new_cishu}") else: - max_cishu = ShenheJilu.objects.filter( + max_cishu = ShenheJilu.query.filter( shenqingren_id=user.yonghuid, chenghao=chenghao, zhuangtai__in=[1, 2] @@ -12068,7 +12070,7 @@ class KaohePayView(APIView): logger.info(f"生成订单ID: {dingdanid}") # 8. 创建支付订单(Czjilu) - order = Czjilu.objects.create( + order = Czjilu.query.create( dingdan_id=dingdanid, zhuangtai=9, # 未支付 yonghuid=user.yonghuid, @@ -12080,7 +12082,7 @@ class KaohePayView(APIView): # 9. 保存考核支付临时上下文(用于回调时取业务参数) try: - KaohePayTemp.objects.create( + KaohePayTemp.query.create( dingdan_id=dingdanid, yonghuid=user.yonghuid, tag_id=tag_id, @@ -12117,7 +12119,7 @@ class KaohePayView(APIView): order.delete() # 下单失败,删除订单记录 # 同时删除临时表记录 try: - KaohePayTemp.objects.filter(dingdan_id=dingdanid).delete() + KaohePayTemp.query.filter(dingdan_id=dingdanid).delete() except Exception as del_e: logger.error(f"删除临时表记录失败: {str(del_e)}") logger.error(f"生成微信支付参数失败: {str(e)}", exc_info=True) @@ -12335,7 +12337,7 @@ class KaohePayCallbackView(View): # 查找订单 try: - order = Czjilu.objects.get(dingdan_id=out_trade_no, leixing=5) + order = Czjilu.query.get(dingdan_id=out_trade_no, leixing=5) logger.info(f"找到订单: {out_trade_no}, 当前状态: {order.zhuangtai}") except Czjilu.DoesNotExist: logger.error(f"考核订单不存在: {out_trade_no}") @@ -12363,10 +12365,10 @@ class KaohePayCallbackView(View): # 从临时表获取业务参数并创建审核记录 try: - temp = KaohePayTemp.objects.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) + temp = KaohePayTemp.query.get(dingdan_id=out_trade_no, yonghuid=order.yonghuid) logger.info(f"找到临时表记录: {out_trade_no}, 用户ID: {order.yonghuid}") - user = UserMain.objects.get(yonghuid=order.yonghuid) + user = UserMain.query.get(yonghuid=order.yonghuid) logger.info(f"获取用户信息成功: {user.yonghuid}") success, msg, record = create_shenhe_jilu_from_temp(user, temp) @@ -12403,7 +12405,7 @@ class KaohePayCallbackView(View): update_daily_income(jine_decimal) logger.info(f"更新每日收入成功: {jine_decimal}元") - szjilu, created = Szjilu.objects.get_or_create( + szjilu, created = Szjilu.query.get_or_create( id=1, defaults={ 'zongsy': Decimal('0.00'), @@ -12483,7 +12485,7 @@ class KaohePayPollView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) if order.zhuangtai == 3: return Response({'code': 200, 'message': '支付成功'}) return Response({'code': 400, 'message': '订单尚未支付'}) @@ -12500,7 +12502,7 @@ class KaohePayFailView(APIView): if not dingdanid: return Response({'code': 400, 'message': '订单ID不能为空'}) try: - order = Czjilu.objects.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) + order = Czjilu.query.get(dingdan_id=dingdanid, yonghuid=request.user.yonghuid, leixing=5) if order.zhuangtai == 9: order.delete() return Response({'code': 200, 'message': '订单已删除'}) @@ -12598,27 +12600,27 @@ class TixianAssetView(APIView): # ------------------- 费率查询(从 Lilubiao 获取) ------------------- # 打手提现费率 (类型 '5') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='5').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='5').first() data['dashou_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 # 打手保证金提现费率 (类型 '11') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='11').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='11').first() data['dashou_yajin_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 # 商家提现费率 (类型 '10') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='10').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='10').first() data['shangjia_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 # 管事提现费率 (类型 '6') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='6').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='6').first() data['guanshi_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 # 组长提现费率 (类型 '8') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='8').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='8').first() data['zuzhang_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 # 审核官(考核官)提现费率 (类型 '9') - lilu_obj = Lilubiao.objects.filter(fadanpingtai='9').first() + lilu_obj = Lilubiao.query.filter(fadanpingtai='9').first() data['kaoheguan_rate'] = float(lilu_obj.lilu) if lilu_obj and lilu_obj.lilu is not None else 0.00 return Response({ @@ -12673,7 +12675,7 @@ class GuanshiContactView(APIView): # 4. 根据邀请人用户ID查询用户主表 try: - inviter_user = UserMain.objects.get(yonghuid=inviter_yonghuid) + inviter_user = UserMain.query.get(yonghuid=inviter_yonghuid) except UserMain.DoesNotExist: logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}") return Response({