feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置

This commit is contained in:
XingQue
2026-06-24 05:02:18 +08:00
parent ce9b09f096
commit dcc4936428
82 changed files with 5748 additions and 924 deletions

View File

@@ -41,12 +41,14 @@ INSTALLED_APPS = [
'shop',
'rank',
'merchant_ops',
'jituan',
]
MIDDLEWARE = [
'utils.ip_filter_middleware.IPFilterMiddleware',
'utils.debug_middleware.DebugExceptionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'jituan.middleware.ClubContextMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@@ -222,6 +224,7 @@ CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
CORS_ALLOW_HEADERS = [
"accept", "accept-encoding", "authorization", "content-type",
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
"x-club-id", "x-club-scope",
]
# ==================== IP 访问控制 ====================

View File

@@ -24,6 +24,7 @@ urlpatterns = [
path('houtai/', include('backend.urls')),
path('shangdian/', include('shop.urls')),
path('dengji/', include('rank.urls')),
path('jituan/', include('jituan.urls')),
]
# 重要说明:

View File

@@ -0,0 +1,28 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('backend', '0002_rename_abnormal_us_create__ee16da_idx_abnormal_us_createt_38f96c_idx'),
]
operations = [
migrations.AddField(
model_name='withdrawaldailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AlterUniqueTogether(
name='withdrawaldailystats',
unique_together=set(),
),
migrations.AlterUniqueTogether(
name='withdrawaldailystats',
unique_together={('club_id', 'Date', 'WithdrawalType')},
),
migrations.AddIndex(
model_name='withdrawaldailystats',
index=models.Index(fields=['club_id', 'Date', 'WithdrawalType'], name='tixian_ri_club_date_type_idx'),
),
]

View File

@@ -0,0 +1,56 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('backend', '0003_withdrawaldailystats_club_id'),
]
operations = [
migrations.AddField(
model_name='merchantdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='playerdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='leaderdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='managerdailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='managerrenewaldailystats',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AlterUniqueTogether(
name='merchantdailystats',
unique_together={('club_id', 'MerchantID', 'Date')},
),
migrations.AlterUniqueTogether(
name='playerdailystats',
unique_together={('club_id', 'PlayerID', 'Date')},
),
migrations.AlterUniqueTogether(
name='leaderdailystats',
unique_together={('club_id', 'LeaderID', 'Date')},
),
migrations.AlterUniqueTogether(
name='managerdailystats',
unique_together={('club_id', 'ManagerID', 'Date')},
),
migrations.AlterUniqueTogether(
name='managerrenewaldailystats',
unique_together={('club_id', 'ManagerID', 'Date')},
),
]

View File

@@ -122,6 +122,7 @@ class AbnormalUserLog(QModel):
# ==================== 6. 商家每日统计 ====================
class MerchantDailyStats(QModel):
"""商家每日统计表(原 ShangjiaRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
MerchantID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='商家用户ID')
Date = models.DateField(
@@ -151,7 +152,7 @@ class MerchantDailyStats(QModel):
class Meta:
db_table = 'shangjia_ri_tongji'
verbose_name = '商家每日统计'
unique_together = [['MerchantID', 'Date']]
unique_together = [['club_id', 'MerchantID', 'Date']]
indexes = [
models.Index(fields=['Date', '-AssignedAmount']),
models.Index(fields=['Date', '-SettledAmount']),
@@ -165,6 +166,7 @@ class MerchantDailyStats(QModel):
# ==================== 7. 打手每日统计 ====================
class PlayerDailyStats(QModel):
"""打手每日统计表(原 DashouRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
PlayerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='打手用户ID')
Date = models.DateField(
@@ -194,7 +196,7 @@ class PlayerDailyStats(QModel):
class Meta:
db_table = 'dashou_ri_tongji'
verbose_name = '打手每日统计'
unique_together = [['PlayerID', 'Date']]
unique_together = [['club_id', 'PlayerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-AcceptedOrderTotal']),
models.Index(fields=['Date', '-CompletedOrderTotal']),
@@ -209,6 +211,7 @@ class PlayerDailyStats(QModel):
# ==================== 8. 组长每日统计 ====================
class LeaderDailyStats(QModel):
"""组长每日统计表(原 ZuzhangRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
LeaderID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='组长用户ID')
Date = models.DateField(
@@ -226,7 +229,7 @@ class LeaderDailyStats(QModel):
class Meta:
db_table = 'zuzhang_ri_tongji'
verbose_name = '组长每日统计'
unique_together = [['LeaderID', 'Date']]
unique_together = [['club_id', 'LeaderID', 'Date']]
indexes = [
models.Index(fields=['Date', '-InvitedManagerCount']),
models.Index(fields=['Date', '-TotalIncome']),
@@ -239,6 +242,7 @@ class LeaderDailyStats(QModel):
# ==================== 9. 管事每日统计 ====================
class ManagerDailyStats(QModel):
"""管事每日统计表(原 GuanshiRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
@@ -255,7 +259,7 @@ class ManagerDailyStats(QModel):
class Meta:
db_table = 'guanshi_ri_tongji'
verbose_name = '管事每日统计'
unique_together = [['ManagerID', 'Date']]
unique_together = [['club_id', 'ManagerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-InvitedPlayerCount']),
models.Index(fields=['Date', '-RechargedPlayerCount']),
@@ -269,6 +273,7 @@ class ManagerDailyStats(QModel):
# ==================== 10. 管事续费每日统计 ====================
class ManagerRenewalDailyStats(QModel):
"""管事续费每日统计表(原 GuanshiXufeiRiTongji"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ManagerID = models.CharField(
max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID')
Date = models.DateField(
@@ -283,7 +288,7 @@ class ManagerRenewalDailyStats(QModel):
class Meta:
db_table = 'guanshi_xufei_ri_tongji'
verbose_name = '管事续费每日统计'
unique_together = [['ManagerID', 'Date']]
unique_together = [['club_id', 'ManagerID', 'Date']]
indexes = [
models.Index(fields=['Date', '-RenewalTotal']),
models.Index(fields=['Date', '-RenewalRevenue']),
@@ -295,7 +300,8 @@ class ManagerRenewalDailyStats(QModel):
# ==================== 11. 提现每日统计 ====================
class WithdrawalDailyStats(QModel):
"""每日提现统计表(原 TixianRiTongji按身份统计当天提现总额"""
"""每日提现统计表(原 TixianRiTongji俱乐部+身份统计当天提现到账总额"""
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
Date = models.DateField(
db_index=True, db_column='riqi', verbose_name='日期')
WithdrawalType = models.IntegerField(
@@ -308,8 +314,11 @@ class WithdrawalDailyStats(QModel):
class Meta:
db_table = 'tixian_ri_tongji'
verbose_name = '每日提现统计'
unique_together = [['Date', 'WithdrawalType']]
indexes = [models.Index(fields=['Date', 'WithdrawalType'])]
unique_together = [['club_id', 'Date', 'WithdrawalType']]
indexes = [
models.Index(fields=['club_id', 'Date', 'WithdrawalType']),
models.Index(fields=['Date', 'WithdrawalType']),
]
def __str__(self):
return f"{self.Date} - {self.get_WithdrawalType_display()} 提现总额: {self.total_amount}"

View File

@@ -32,7 +32,7 @@ class PopupPageSerializer(serializers.ModelSerializer):
class Meta:
model = PopupPage
fields = [
'id', 'page_key', 'name', 'description',
'id', 'club_id', 'page_key', 'name', 'description',
'is_active', 'ignore_user_mute',
'created_at', 'updated_at', 'popups'
]

View File

@@ -21,6 +21,16 @@ from gvsdsdk.fluent import db, func, FQ
logger = logging.getLogger(__name__)
def _club_id_for_yonghuid(yonghuid, club_id=None):
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_user import get_user_club_id
from users.business_models import User
if club_id:
return club_id
u = User.query.filter(UserUID=yonghuid).first()
return get_user_club_id(u) if u else CLUB_ID_DEFAULT
def _get_real_ip(request):
"""获取真实IP考虑代理"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
@@ -150,26 +160,24 @@ def verify_kefu_permission(request, username_from_frontend=None):
def update_dashou_daily_by_action(yonghuid, amount, action):
def update_dashou_daily_by_action(yonghuid, amount, action, club_id=None):
"""
根据行为类型更新打手每日统计(接单、成交、退款)
参数:
yonghuid: 打手用户ID
amount: 本单涉及的金额Decimal
action: 1 = 接单, 2 = 成交(结算), 3 = 退款
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return # 金额非正不统计
return
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = PlayerDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
PlayerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'PlayerID': yonghuid,
'Date': today,
'AcceptedOrderTotal': 0,
@@ -198,33 +206,24 @@ def update_dashou_daily_by_action(yonghuid, amount, action):
PlayerDailyStats.query.filter(id=stat.id).update(**update_fields)
def update_shangjia_daily(yonghuid, amount, action, order_id=None):
def update_shangjia_daily(yonghuid, amount, action, order_id=None, club_id=None):
"""
根据操作行为更新商家每日统计(派发、结算、退款)
参数:
yonghuid: 商家用户ID
amount: 当前订单涉及的金额Decimal
action: 操作类型1 = 派发2 = 结算成交3 = 退款
order_id: 订单ID传入时同步更新该订单派单归属客服的日统计
说明:
- 金额必须大于0否则不执行任何操作
- 使用 select_for_update + F() 表达式保证原子性和并发安全
- 每天每个商家只会有一条统计记录,不存在则自动创建
"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
return # 金额非正,不统计
return
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
# 获取或创建当天统计记录,同时加锁防止并发
stat, created = MerchantDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
MerchantID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'MerchantID': yonghuid,
'Date': today,
'AssignedOrderCount': 0,
@@ -262,18 +261,20 @@ def update_shangjia_daily(yonghuid, amount, action, order_id=None):
logger.warning('同步客服日统计失败 order_id=%s', order_id, exc_info=True)
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00'), club_id=None):
"""
根据操作行为更新管事每日统计(邀请打手、充值会员、订单/押金分红)
action: 1 = 邀请打手, 2 = 充值会员, 3 = 商家订单分红, 4 = 押金分红
"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = ManagerDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
ManagerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'ManagerID': yonghuid,
'Date': today,
'InvitedPlayerCount': 0,
@@ -319,17 +320,17 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
)
def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')):
"""
管事续费统计(单独表)
用法: update_guanshi_xufei_daily('300001', Decimal('30'))
"""
def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00'), club_id=None):
"""管事续费统计(单独表)"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
ManagerID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'ManagerID': yonghuid,
'Date': today,
'RenewalTotal': 1,
@@ -343,24 +344,18 @@ def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')):
)
def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
"""
根据操作行为更新组长每日统计(邀请管事、分佣收入)
参数:
yonghuid: 组长用户ID
action: 操作类型
1 = 邀请管事(次数+1无金额
2 = 分佣收入(累加金额)
amount: 分佣金额Decimal仅在 action=2 时有效
"""
def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00'), club_id=None):
"""根据操作行为更新组长每日统计"""
cid = _club_id_for_yonghuid(yonghuid, club_id)
today = date.today()
with transaction.atomic():
stat, created = LeaderDailyStats.objects.select_for_update().get_or_create(
club_id=cid,
LeaderID=yonghuid,
Date=today,
defaults={
'club_id': cid,
'LeaderID': yonghuid,
'Date': today,
'InvitedManagerCount': 0,
@@ -394,20 +389,19 @@ def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')):
def update_tixian_daily_stat(leixing, amount):
def update_tixian_daily_stat(leixing, amount, club_id=None):
"""
更新每日提现统计(平台维度)
更新每日提现统计(平台维度,按俱乐部
leixing: 1=打手, 2=管事, 3=组长
amount: 本次提现金额(申请金额,含手续费)
"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.withdraw_config import get_or_create_platform_daily_stat
club_id = (club_id or '').strip() or CLUB_ID_DEFAULT
today = date.today()
with transaction.atomic():
stat, created = WithdrawalDailyStats.objects.select_for_update().get_or_create(
Date=today,
WithdrawalType=leixing,
defaults={'total_amount': amount}
)
if not created:
stat = get_or_create_platform_daily_stat(club_id, leixing, today)
WithdrawalDailyStats.query.filter(id=stat.id).update(
total_amount=F('total_amount') + amount
)
@@ -561,13 +555,14 @@ XIUGAI_LEIXING_LABEL = {
}
def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, **extra_fields):
def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, club_id=None, request_ip='', **extra_fields):
"""
统一写入 xiugaijilu 修改记录。
qitashuoming 应详细描述本次操作(尤其是金额、身份、上下级变更等)。
extra_fields 可传入 xiugaitijiao、shangjiayue、guanshiyue 等模型字段。
"""
from users.models import Xiugaijilu
from jituan.services.admin_audit import log_admin_audit_from_xiugai
if not qitashuoming:
label = XIUGAI_LEIXING_LABEL.get(leixing, f'类型{leixing}')
@@ -580,3 +575,11 @@ def write_xiugai_log(*, yonghuid, xiugaiid, leixing, qitashuoming, **extra_field
qitashuoming=qitashuoming,
**extra_fields,
)
log_admin_audit_from_xiugai(
yonghuid=yonghuid,
xiugaiid=xiugaiid,
leixing=leixing,
qitashuoming=qitashuoming,
club_id=club_id,
request_ip=request_ip,
)

View File

@@ -38,7 +38,12 @@ from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from orders.utils import update_daily_payout
from jituan.services.club_context import filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
@@ -743,7 +748,9 @@ class KefuGetDashouListView(APIView):
return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
# 4. 构建打手查询集(仅打手类型)
dashou_qs = UserDashou.query.select_related('user').all()
dashou_qs = filter_user_related_by_club(
UserDashou.query.select_related('user').all(), request,
)
# 关键词筛选用户ID或昵称
if keyword:
@@ -758,12 +765,12 @@ class KefuGetDashouListView(APIView):
# 6. 统计有效打手数(有会员且未过期)
dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True)
now = timezone.now()
valid_user_ids = Huiyuangoumai.query.filter(
hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request)
valid_dashou_count = hy_qs.filter(
yonghu_id__in=dashou_user_ids,
huiyuan_zhuangtai=1,
daoqi_time__gt=now
).values_list('yonghu_id', flat=True).distinct()
valid_dashou_count = valid_user_ids.count()
daoqi_time__gt=now,
).values('yonghu_id').distinct().count()
# 7. 分页
offset = (page - 1) * page_size
@@ -784,8 +791,11 @@ class KefuGetDashouListView(APIView):
has_more = (offset + len(dashou_list)) < total_dashou
meta = list_response_meta(request)
return Response({
'code': 0,
'club_id': meta['club_id'],
'scope': meta['scope'],
'data': {
'list': result,
'has_more': has_more,
@@ -836,6 +846,10 @@ class KefuGetDashouDetailView(APIView):
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
denied = forbid_if_user_out_of_scope(request, user_main)
if denied:
return denied
try:
dashou = user_main.DashouProfile
except ObjectDoesNotExist:
@@ -1346,7 +1360,9 @@ class KefuGetShangjiaListView(APIView):
return Response({'code': 403, 'msg': '您没有权限查看商家列表'})
# 4. 构建商家查询集(商家扩展表 + 关联用户主表)
shangjia_qs = UserShangjia.query.select_related('user').all()
shangjia_qs = filter_user_related_by_club(
UserShangjia.query.select_related('user').all(), request,
)
# 关键词筛选用户ID或昵称
if keyword:
@@ -1393,6 +1409,7 @@ class KefuGetShangjiaListView(APIView):
# 5. 统计总商家数(应用筛选后)
total = shangjia_qs.count()
valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count()
# 6. 分页
offset = (page - 1) * page_size
@@ -1412,11 +1429,15 @@ class KefuGetShangjiaListView(APIView):
'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
})
meta = list_response_meta(request)
return Response({
'code': 0,
'club_id': meta['club_id'],
'scope': meta['scope'],
'data': {
'list': result,
'total': total,
'valid_merchant_count': valid_merchant_count,
}
})
@@ -1466,6 +1487,10 @@ class KefuGetShangjiaDetailView(APIView):
except UserShangjia.DoesNotExist:
return Response({'code': 404, 'msg': '商家不存在'})
denied = forbid_if_user_out_of_scope(request, shangjia.user)
if denied:
return denied
user = shangjia.user
data = {
@@ -2847,9 +2872,12 @@ class GetGuanliListView(APIView):
status = request.data.get('status')
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss左连接获取昵称
qs = User.query.filter(
qs = filter_user_qs_by_club(
User.query.filter(
GuanshiProfile__isnull=False # 确保是管事
).select_related('GuanshiProfile').select_related('BossProfile')
).select_related('GuanshiProfile').select_related('BossProfile'),
request,
)
# 关键词搜索用户ID 或 昵称来自 boss_profile.nickname
if keyword:
@@ -2929,7 +2957,11 @@ class GetGuanliListView(APIView):
'chongzhifenrun': str(guanshi.chongzhifenrun),
})
return Response({'code': 0, 'data': {'list': result, 'total': total}})
return Response({
'code': 0,
**list_response_meta(request),
'data': {'list': result, 'total': total},
})
@@ -2984,6 +3016,10 @@ class GetGuanliDetailView(APIView):
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
base_data = {
@@ -3377,8 +3413,12 @@ class UpdateGuanliView(APIView):
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 获取当前所有首次定制记录
existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1)
existing_first = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
)
# 前端传来的定制字典 {会员ID: 金额}
for huiyuan_id, amount in custom_first.items():
try:
@@ -3386,17 +3426,19 @@ class UpdateGuanliView(APIView):
except:
continue
if amount <= 0:
# 金额为0或负数表示删除定制
DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
defaults={
'guanshi_fenhong': amount,
'zuzhang_fenhong': Decimal('0')
}
'zuzhang_fenhong': Decimal('0'),
},
)
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first:
@@ -3405,6 +3447,8 @@ class UpdateGuanliView(APIView):
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 收集需要保留的记录
keep_set = set()
for times_str, members in extra_map.items():
@@ -3429,6 +3473,7 @@ class UpdateGuanliView(APIView):
keep_set.add((cishu, huiyuan_id))
# 更新或创建
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
@@ -3438,7 +3483,9 @@ class UpdateGuanliView(APIView):
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2)
existing_extra = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
@@ -3493,53 +3540,8 @@ class GetWithdrawSettingsView(APIView):
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限查看提现设置'})
# 1. 手续费利率CommissionRate5打手/6管事/8组长/9审核官/10商家/11打手押金
rate_map = {}
for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
obj = CommissionRate.query.filter(Platform=code).first()
rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0
# 1b. 订单/押金分红费率CommissionRate1平台订单/3商家订单/12押金管事/13商家订单管事分红
order_rate_map = {}
for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]:
obj = CommissionRate.query.filter(Platform=code).first()
order_rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0
# 2. 个人每日限额 (leixing=1,2,3)
quota_map = {}
for leixing in [1, 2, 3]:
obj = TixianQuotaDefault.query.filter(UserType=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.query.filter(UserType=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 = WithdrawalDailyStats.query.filter(Date=today)
for rec in records:
if rec.WithdrawalType in today_totals:
today_totals[rec.WithdrawalType] = float(rec.total_amount)
# 🆕 获取提现模式自动1 / 手动2默认2
config = WithdrawConfig.query.filter(id=1).first()
withdraw_mode = config.mode if config else 2
return Response({
'code': 0,
'data': {
'withdraw_mode': withdraw_mode,
'rates': rate_map,
'order_rates': order_rate_map,
'quotas': quota_map,
'total_limits': total_limit_map,
'today_totals': today_totals,
}
})
from jituan.services.withdraw_config import build_withdraw_settings_payload
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
class UpdateWithdrawSettingsView(APIView):
@@ -3566,124 +3568,8 @@ class UpdateWithdrawSettingsView(APIView):
# 权限映射:要修改的角色需要对应的权限
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
rates = request.data.get('rates', {})
order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
quotas = request.data.get('quotas', {})
total_limits = request.data.get('total_limits', {})
# 角色类型映射
role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
# 利率代码映射1-3 有水缸限额4审核官/5打手押金/6商家余额仅费率
rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
# 订单/押金分红1平台订单/3商家订单/12押金管事/13商家订单管事分红
order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'}
# 总限额:提现类型 → TixianQuotaDefault.leixing4~9
total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
def _rate_key_present(data, role):
return (str(role) in data and data[str(role)] is not None) or \
(role in data and data[role] is not None)
with transaction.atomic():
# 遍历三个角色(限额相关)
for role in [1, 2, 3]:
perm_needed = role_perms[role]
if perm_needed not in permissions:
if _rate_key_present(rates, role) or \
(str(role) in quotas and quotas[str(role)] is not None) or \
(role in quotas and quotas.get(role) is not None) or \
(str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed}'})
# 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一
extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
for role in [4, 5, 6]:
if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
if (str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
# 订单/押金分红费率:需 5500a/b/c 任一
for role in [1, 3, 12, 13]:
if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
order_names = {
1: '平台订单打手分红', 3: '商家订单打手分红',
12: '押金管事分红', 13: '管事订单分红',
}
return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
# 1. 修改手续费利率
for role in [1, 2, 3, 4, 5, 6]:
val = rates.get(str(role), rates.get(role))
if val is not None:
rate = Decimal(str(val))
code = rate_code_map[role]
obj, created = CommissionRate.objects.select_for_update().get_or_create(
Platform=code,
defaults={'Rate': rate}
)
if not created:
obj.Rate = rate
obj.save()
# 1b. 修改订单/押金分红费率
for role in [1, 3, 12, 13]:
val = order_rates.get(str(role), order_rates.get(role))
if val is not None:
rate = Decimal(str(val))
code = order_rate_code_map[role]
obj, created = CommissionRate.objects.select_for_update().get_or_create(
Platform=code,
defaults={'Rate': rate}
)
if not created:
obj.Rate = rate
obj.save()
# 2. 修改个人每日限额
for role in [1, 2, 3]:
if str(role) in quotas:
val = quotas[str(role)]
if val is not None:
quota = Decimal(str(val))
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
UserType=role,
defaults={'default_quota': quota}
)
if not created:
obj.default_quota = quota
obj.save()
# 3. 修改每日总限额1打手/2管事/3组长/4考核官/5押金/6商家
for role in [1, 2, 3, 4, 5, 6]:
val = total_limits.get(str(role), total_limits.get(role))
if val is not None:
limit = Decimal(str(val))
code = total_code_map[role]
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
UserType=code,
defaults={'default_quota': limit}
)
if not created:
obj.default_quota = limit
obj.save()
# 🆕 修改提现模式有5500a/b/c任一即可
withdraw_mode = request.data.get('withdraw_mode')
if withdraw_mode is not None:
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限修改提现模式'})
# 更新或创建 id=1 的配置
WithdrawConfig.query.update_or_create(
id=1,
defaults={'mode': int(withdraw_mode)}
)
# 注意WithdrawalDailyStats 表由提现流程自动更新,此处不直接修改
return Response({'code': 0, 'msg': '设置修改成功'})
from jituan.services.withdraw_config import apply_withdraw_settings_update
return apply_withdraw_settings_update(request, permissions)
@@ -3738,7 +3624,10 @@ class GetZuzhangListView(APIView):
commission_value = request.data.get('commission_value')
# 5. 构建基础 QuerySet预加载关联表
queryset = UserZuzhang.query.select_related('user', 'user__BossProfile').all()
queryset = filter_user_related_by_club(
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
request,
)
# 6. 应用筛选条件(使用 ORM 安全过滤)
if keyword:
@@ -3808,6 +3697,7 @@ class GetZuzhangListView(APIView):
return Response({
'code': 0,
'msg': 'success',
**list_response_meta(request),
'data': {
'list': data_list,
'total': total,
@@ -3869,6 +3759,10 @@ class GetZuzhangDetailView(APIView):
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
base_data = {
'yonghuid': user.UserUID,
@@ -4134,7 +4028,11 @@ class UpdateZuzhangView(APIView):
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
existing_first = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
)
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
@@ -4142,9 +4040,12 @@ class UpdateZuzhangView(APIView):
continue
if amount <= 0:
# 删除定制(恢复默认)
DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
@@ -4160,6 +4061,8 @@ class UpdateZuzhangView(APIView):
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set()
for times_str, members in extra_map.items():
try:
@@ -4182,6 +4085,7 @@ class UpdateZuzhangView(APIView):
continue
keep_set.add((cishu, huiyuan_id))
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
@@ -4191,7 +4095,9 @@ class UpdateZuzhangView(APIView):
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2)
existing_extra = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
@@ -4367,6 +4273,8 @@ class GetOperationLogListView(APIView):
page_size = 20
qs = Xiugaijilu.query.all().order_by('-CreateTime')
from jituan.services.admin_audit import filter_xiugaijilu_by_request
qs = filter_xiugaijilu_by_request(qs, request)
if yonghuid:
qs = qs.filter(yonghuid=yonghuid)
if xiugaiid:
@@ -4412,6 +4320,7 @@ class GetOperationLogListView(APIView):
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
@@ -4419,6 +4328,7 @@ class GetOperationLogListView(APIView):
'total': total,
'page': page,
'page_size': page_size,
**list_response_meta(request),
},
})
@@ -4995,11 +4905,19 @@ class PopupNoticeListAPIView(APIView):
return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
# 4. 查询所有页面并预加载关联的弹窗和图片避免N+1查询
pages = PopupPage.query.all().prefetch_related('popups__images').order_by('id')
from jituan.services.display_config import filter_popup_page_by_request, list_response_meta
pages = filter_popup_page_by_request(
PopupPage.query.all().prefetch_related('popups__images'),
request,
).order_by('id')
# 5. 序列化输出
serializer = PopupPageSerializer(pages, many=True)
return Response({'code': 0, 'data': {'pages': serializer.data}})
return Response({
'code': 0,
'data': {'pages': serializer.data, **list_response_meta(request)},
})
class PopupNoticeModifyAPIView(APIView):
@@ -5020,6 +4938,11 @@ class PopupNoticeModifyAPIView(APIView):
if REQUIRED_PERMISSION not in permissions:
return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
# ---------- 2. 获取 action 并分发 ----------
action = request.data.get('action')
if not action:
@@ -5055,15 +4978,24 @@ class PopupNoticeModifyAPIView(APIView):
def add_page(self, request):
"""添加新页面"""
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
data = request.data
# 必填字段校验
if not data.get('page_key') or not data.get('name'):
return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
# 唯一性校验
if PopupPage.query.filter(page_key=data['page_key']).exists():
club_id = resolve_club_id_from_request(request)
# 唯一性校验(同俱乐部内)
if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists():
return Response({'code': 400, 'msg': '页面标识已存在'})
page = PopupPage.query.create(
club_id=club_id,
page_key=data['page_key'],
name=data['name'],
description=data.get('description', ''),
@@ -5082,6 +5014,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
# 更新允许修改的字段
page.page_key = request.data.get('page_key', page.page_key)
page.name = request.data.get('name', page.name)
@@ -5102,6 +5039,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
if cascade:
# 级联删除:使用事务保证原子性
with transaction.atomic():
@@ -5135,6 +5077,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
# 同一页面下 popup_id 必须唯一
if PopupConfig.query.filter(page=page, popup_id=popup_id).exists():
return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
@@ -5166,6 +5113,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
# 更新弹窗字段
popup.popup_id = request.data.get('popup_id', popup.popup_id)
popup.title = request.data.get('title', popup.title)
@@ -5192,6 +5144,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
with transaction.atomic():
for img in popup.images.all():
if img.image_url:
@@ -5237,6 +5194,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
# 获取上传的文件
file_obj = request.FILES.get('file')
if not file_obj:
@@ -5267,6 +5229,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupImage.DoesNotExist:
return Response({'code': 404, 'msg': '图片不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
if deny:
return deny
# 更新文字和排序
image.text = request.data.get('text', image.text)
image.sort_order = request.data.get('sort_order', image.sort_order)
@@ -5297,6 +5264,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupImage.DoesNotExist:
return Response({'code': 404, 'msg': '图片不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
if deny:
return deny
if image.image_url:
delete_from_oss(image.image_url)
image.delete()
@@ -6191,7 +6163,7 @@ class FaKuanTongJiView(APIView):
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
stats = Penalty.query.aggregate(
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(Status=1)),
shensuzhong=Count('id', filter=Q(Status=3)),
@@ -6199,7 +6171,8 @@ class FaKuanTongJiView(APIView):
yibohui=Count('id', filter=Q(Status=4)),
)
data = {k: v or 0 for k, v in stats.items()}
return Response({'code': 0, 'msg': '成功', 'data': data})
from jituan.services.club_user_access import list_response_meta
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
except Exception as e:
logger.error(f"罚款统计异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
@@ -6230,7 +6203,7 @@ class FaKuanLieBiaoView(APIView):
if page_size > 100:
page_size = 100
qs = Penalty.query.all()
qs = filter_penalty_qs(Penalty.query.all(), request)
# 状态筛选
zhuangtai = request.data.get('zhuangtai')
@@ -6309,6 +6282,7 @@ class FaKuanLieBiaoView(APIView):
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0, 'msg': '成功',
'data': {
@@ -6316,6 +6290,7 @@ class FaKuanLieBiaoView(APIView):
'total': paginator.count,
'page': page,
'page_size': page_size,
**list_response_meta(request),
}
})
except Exception as e:
@@ -6351,6 +6326,10 @@ class FaKuanChuLiView(APIView):
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != 3:
return Response({'code': 400, 'msg': '当前状态不可处理'})
@@ -6479,6 +6458,9 @@ class FaKuanChuangJianView(APIView):
FineAmount=FineAmount,
AffectsGrabbing=AffectsGrabbing,
Status=1, # 待缴纳
ClubID=resolve_penalty_club_id(
penalized_user_id=PenalizedUserID, request=request,
),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
@@ -6580,6 +6562,7 @@ class FineApplyView(APIView):
Status=1, # 待缴纳
AffectsGrabbing=AffectsGrabbing,
ApplicantIdentity=1, # 申请人身份1 客服
ClubID=resolve_penalty_club_id(order=order, request=request),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
@@ -6658,6 +6641,7 @@ class PunishDashouView(APIView):
ApplyStatus=0,
DeductedPoints=jifen,
OrderID=OrderID,
ClubID=resolve_penalty_record_club_id(order_id=OrderID, player_id=dashou_id),
)
# 扣除打手积分
dashou.jifen = F('jifen') - jifen
@@ -7158,7 +7142,9 @@ class HybkjtsjView(APIView):
}
# 充值分组查询
chongzhi_qs = Czjilu.query.filter(**chongzhi_filter) \
chongzhi_qs = filter_queryset_by_club(
Czjilu.query.filter(**chongzhi_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
@@ -7174,7 +7160,9 @@ class HybkjtsjView(APIView):
fenhong_agg['guanshi_amount'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
fenhong_qs = Gsfenhong.query.filter(**fenhong_filter) \
fenhong_qs = filter_gsfenhong_qs(
Gsfenhong.query.filter(**fenhong_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(**fenhong_agg) \
@@ -7347,13 +7335,16 @@ class SzxxView(APIView):
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
# ---------- 收入分组查询 ----------
income_qs = DailyIncomeStat.query.filter(**income_filter) \
.values(group_field) \
income_qs = filter_queryset_by_club(
DailyIncomeStat.query.filter(**income_filter), request,
).values(group_field) \
.annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
.order_by(group_field)
# ---------- 支出分组查询 ----------
payout_qs = DailyPayoutStat.query.filter(**payout_filter) \
payout_qs = filter_queryset_by_club(
DailyPayoutStat.query.filter(**payout_filter), request,
).values(group_field) \
.values(group_field) \
.annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \
.order_by(group_field)
@@ -7395,10 +7386,14 @@ class SzxxView(APIView):
time_series.sort(key=lambda x: x['date'])
# ---------- 汇总数据 ----------
income_summary = DailyIncomeStat.query.filter(**summary_filter).aggregate(
income_summary = filter_queryset_by_club(
DailyIncomeStat.query.filter(**summary_filter), request,
).aggregate(
total_income=Sum('total_amount'), total_count=Sum('total_count')
)
payout_summary = DailyPayoutStat.query.filter(**summary_filter).aggregate(
payout_summary = filter_queryset_by_club(
DailyPayoutStat.query.filter(**summary_filter), request,
).aggregate(
total_payout=Sum('total_amount'), total_count=Sum('total_count')
)
total_income = float(income_summary['total_income'] or 0)
@@ -7542,7 +7537,10 @@ class CwhqjtddsjView(APIView):
filters['Platform'] = order_source
# ---------- 时间序列分组查询 ----------
qs = Order.query.filter(**filters) \
order_base = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
qs = order_base \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
@@ -7595,7 +7593,9 @@ class CwhqjtddsjView(APIView):
})
# ---------- 汇总数据 ----------
summary_qs = Order.query.filter(**filters)
summary_qs = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
total_order_count = summary_qs.count()
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
total_success_count = summary_qs.filter(Status=3).count()
@@ -7728,11 +7728,14 @@ 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.query.filter(
cz_qs = filter_queryset_by_club(
Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3 # 只统计支付成功的
zhuangtai=3,
),
request,
).annotate(
period=trunc_func
).values('period', 'leixing').annotate(
@@ -7762,10 +7765,13 @@ class CwqtczhqView(APIView):
# 2. 处理罚款Penalty—— 修改:直接从 Penalty 表获取分红总额和分红笔数
if 6 in types:
# 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数
penalty_qs = Penalty.query.filter(
penalty_qs = filter_penalty_qs(
Penalty.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
Status=2 # 已缴纳
Status=2,
),
request,
).annotate(
period=trunc_func
).values('period').annotate(
@@ -7816,7 +7822,9 @@ class CwqtczhqView(APIView):
filter_kwargs['CreateTime__month'] = summary_q.month
else:
filter_kwargs['CreateTime__year'] = summary_q.year
agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
agg = filter_queryset_by_club(
Czjilu.query.filter(**filter_kwargs), request,
).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
@@ -7845,7 +7853,9 @@ class CwqtczhqView(APIView):
penalty_filter['CreateTime__year'] = summary_q.year
# 一次聚合:罚款笔数、金额、分红总额、分红笔数
agg = Penalty.query.filter(**penalty_filter).aggregate(
agg = filter_penalty_qs(
Penalty.query.filter(**penalty_filter), request,
).aggregate(
count=Count('id'),
amount=Sum('FineAmount'),
fenhong_amount=Sum('ApplicantBonusAmount'),
@@ -8073,7 +8083,9 @@ class CwqtczhqView(APIView):
filter_kwargs['CreateTime__month'] = summary_q.month
else:
filter_kwargs['CreateTime__year'] = summary_q.year
agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
agg = filter_queryset_by_club(
Czjilu.query.filter(**filter_kwargs), request,
).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
@@ -8422,7 +8434,10 @@ class KhgglView(APIView):
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
# 基础查询:所有审核官,预加载用户及打手扩展表
queryset = UserShenheguan.query.select_related('user__DashouProfile')
queryset = filter_user_related_by_club(
UserShenheguan.query.select_related('user__DashouProfile'),
request,
)
if filter_yonghuid:
queryset = queryset.filter(user__UserUID=filter_yonghuid)
@@ -8446,14 +8461,19 @@ class KhgglView(APIView):
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 预计算每个板块的打手数量 ----------
bankuai_dashou_count_map = {}
all_banquai = Bankuai.query.all()
for bk in all_banquai:
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
count = YonghuChenghao.query.filter(
count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao_id__in=tag_ids,
yonghu__DashouProfile__isnull=False
yonghu__DashouProfile__isnull=False,
),
request,
).values('yonghu').distinct().count()
bankuai_dashou_count_map[bk.bankuai_id] = count
@@ -8463,7 +8483,10 @@ class KhgglView(APIView):
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
user_count = YonghuChenghao.query.filter(chenghao=tag).values('yonghu').distinct().count()
user_count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao=tag),
request,
).values('yonghu').distinct().count()
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
@@ -8772,6 +8795,7 @@ class ZxkfghdsView(APIView):
Remark=old_order.Remark,
User1ID=old_order.User1ID, # 商家标识
ImageURL=old_order.ImageURL,
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
# 注意:不继承 AssignedID、PlayerID
)
@@ -9022,13 +9046,24 @@ class KhjlglView(APIView):
if page_size > 100:
page_size = 100
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
filter_data = request.data
qs = _build_shenhe_jilu_filters(filter_data)
qs = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
stats_base = _build_shenhe_jilu_filters(filter_data)
stats_base = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
tag_rows = _build_shenhe_jilu_filters(filter_data).values(
tag_rows = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
).values(
'chenghao_id', 'chenghao__mingcheng'
).annotate(
total=Count('jilu_id'),
@@ -9040,10 +9075,10 @@ class KhjlglView(APIView):
stats = {
'total': stats_base.count(),
'passed': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=1).count(),
'failed': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=2).count(),
'in_progress': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=0).count(),
'pending': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=3).count(),
'passed': stats_base.filter(zhuangtai=1).count(),
'failed': stats_base.filter(zhuangtai=2).count(),
'in_progress': stats_base.filter(zhuangtai=0).count(),
'pending': stats_base.filter(zhuangtai=3).count(),
'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [
{

View File

@@ -0,0 +1,47 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0003_remove_shangjialianjie_shangjia_li_yonghu__9f2b25_idx_and_more'),
]
operations = [
migrations.AddField(
model_name='dailyincomestat',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
),
migrations.AddField(
model_name='dailypayoutstat',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
),
migrations.AlterField(
model_name='dailyincomestat',
name='date',
field=models.DateField(db_index=True, verbose_name='统计日期'),
),
migrations.AlterField(
model_name='dailypayoutstat',
name='date',
field=models.DateField(db_index=True, verbose_name='统计日期'),
),
migrations.AddIndex(
model_name='dailyincomestat',
index=models.Index(fields=['club_id', 'date'], name='idx_income_club_date'),
),
migrations.AddIndex(
model_name='dailypayoutstat',
index=models.Index(fields=['club_id', 'date'], name='idx_payout_club_date'),
),
migrations.AddConstraint(
model_name='dailyincomestat',
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_income_club_date'),
),
migrations.AddConstraint(
model_name='dailypayoutstat',
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_payout_club_date'),
),
]

View File

@@ -0,0 +1,37 @@
from django.db import migrations, models
def backfill_szjilu_club_id(apps, schema_editor):
Szjilu = apps.get_model('config', 'Szjilu')
for row in Szjilu.objects.all():
if not getattr(row, 'club_id', None):
row.club_id = 'xq'
row.save(update_fields=['club_id'])
class Migration(migrations.Migration):
dependencies = [
('config', '0004_daily_stat_club_id'),
]
operations = [
migrations.AddField(
model_name='szjilu',
name='club_id',
field=models.CharField(
db_index=True,
default='xq',
max_length=16,
verbose_name='俱乐部ID',
),
),
migrations.RunPython(backfill_szjilu_club_id, migrations.RunPython.noop),
migrations.AddConstraint(
model_name='szjilu',
constraint=models.UniqueConstraint(
fields=('club_id',),
name='uniq_szjilu_club_id',
),
),
]

View File

@@ -0,0 +1,60 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('config', '0005_szjilu_club_id'),
]
operations = [
migrations.AddField(
model_name='gonggao',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='lunbo',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='lunbo',
name='page_key',
field=models.CharField(
db_index=True, default='order_pool', max_length=32,
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
),
),
migrations.AddField(
model_name='popuppage',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='tupianpeizhi',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AlterUniqueTogether(
name='popuppage',
unique_together=set(),
),
migrations.AlterField(
model_name='popuppage',
name='page_key',
field=models.CharField(db_index=True, max_length=50, verbose_name='页面标识'),
),
migrations.AlterUniqueTogether(
name='popuppage',
unique_together={('club_id', 'page_key')},
),
migrations.AlterUniqueTogether(
name='tupianpeizhi',
unique_together={('club_id', 'ImageType')},
),
migrations.AddIndex(
model_name='lunbo',
index=models.Index(fields=['club_id', 'page_key', 'ImageType'], name='lunbo_club_page_type_idx'),
),
]

View File

@@ -3,6 +3,11 @@ from gvsdsdk.model_base import QModel
class Lunbo(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
page_key = models.CharField(
max_length=32, default='order_pool', db_index=True,
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
)
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='轮播图片URL')
ImageType = models.IntegerField(null=True, blank=True, verbose_name='图片类型:1轮播2个人中心背景')
@@ -12,10 +17,13 @@ class Lunbo(QModel):
db_table = 'lunbo'
verbose_name = '轮播'
verbose_name_plural = '轮播'
# 不要索引!数据量太小
indexes = [
models.Index(fields=['club_id', 'page_key', 'ImageType']),
]
class Gonggao(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
NoticeType = models.IntegerField(null=True, blank=True, verbose_name='公告类型')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更改时间')
@@ -37,6 +45,7 @@ class Qunpeizhi(QModel):
db_table = 'qunpeizhi'
class Tupianpeizhi(QModel):
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
ImageURL = models.CharField(max_length=500, null=True, blank=True, verbose_name='配置图片URL')
ImageType = models.PositiveIntegerField(null=True, blank=True, verbose_name='提现类型1:打手规则图片2默认头像图片')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
@@ -46,10 +55,12 @@ class Tupianpeizhi(QModel):
db_table = 'tupianpeizhi'
verbose_name = '配置图片'
verbose_name_plural = '配置图片'
unique_together = [['club_id', 'ImageType']]
class Szjilu(QModel):
id = models.AutoField(primary_key=True, verbose_name='记录ID')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
TotalIncome = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总收益')
TotalFlow = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总流水')
TotalExpense = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='总支出')
@@ -61,6 +72,9 @@ class Szjilu(QModel):
db_table = 'szjilu'
verbose_name = '收支记录'
verbose_name_plural = '收支记录'
constraints = [
models.UniqueConstraint(fields=['club_id'], name='uniq_szjilu_club_id'),
]
@@ -277,7 +291,10 @@ class DailyIncomeStat(QModel):
记录平台每天通过微信支付获得的总收入和总笔数
"""
# 日期相关字段(便于快速筛选和分组)
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
date = models.DateField(verbose_name='统计日期', db_index=True)
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
@@ -297,6 +314,10 @@ class DailyIncomeStat(QModel):
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
models.Index(fields=['club_id', 'date']),
]
constraints = [
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_income_club_date'),
]
def __str__(self):
@@ -310,7 +331,10 @@ class DailyPayoutStat(QModel):
每日出款统计表
记录平台每天通过提现/结算等方式的出款总额和总笔数
"""
date = models.DateField(verbose_name='统计日期', db_index=True, unique=True)
date = models.DateField(verbose_name='统计日期', db_index=True)
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
year = models.PositiveSmallIntegerField(verbose_name='年份')
month = models.PositiveSmallIntegerField(verbose_name='月份')
day = models.PositiveSmallIntegerField(verbose_name='日期')
@@ -328,6 +352,10 @@ class DailyPayoutStat(QModel):
indexes = [
models.Index(fields=['date']),
models.Index(fields=['year', 'month', 'day']),
models.Index(fields=['club_id', 'date']),
]
constraints = [
models.UniqueConstraint(fields=['date', 'club_id'], name='uniq_daily_payout_club_date'),
]
def __str__(self):
@@ -337,7 +365,8 @@ class DailyPayoutStat(QModel):
class PopupPage(QModel):
page_key = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='页面标识')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
page_key = models.CharField(max_length=50, db_index=True, verbose_name='页面标识')
name = models.CharField(max_length=100, verbose_name='页面名称')
description = models.TextField(blank=True, verbose_name='描述')
is_active = models.BooleanField(default=True, verbose_name='是否启用')
@@ -349,6 +378,7 @@ class PopupPage(QModel):
db_table = 'popup_page'
verbose_name = '弹窗页面'
verbose_name_plural = '弹窗页面'
unique_together = [['club_id', 'page_key']]
def __str__(self):
return f'{self.name} ({self.page_key})'

View File

@@ -1,75 +1,33 @@
"""
阿龙电竞 - 收支记录定时任务
处理收支记录表的每日清零
固定使用ID=1的记录
收支记录定时任务:每日清零各俱乐部今日流水/今日支出。
"""
from celery import shared_task
from django.db import transaction
from django.utils import timezone
from datetime import datetime
from decimal import Decimal
import logging
# 导入模型
from config.models import Szjilu
from jituan.services.szjilu_accounting import reset_all_daily_szjilu
from utils.celery_utils import log_task_execution, rollback_on_failure
logger = logging.getLogger(__name__)
@shared_task
@rollback_on_failure("收支记录每日清零")
def daily_sz_reset_task():
"""
收支记录每日清零 - 每天凌晨0点5分执行
清零收支记录表的今日相关字段固定使用ID=1的记录
"""
"""收支记录每日清零 - 每天凌晨0点5分执行。"""
try:
with transaction.atomic():
# 获取今天的日期
today = datetime.now().strftime('%Y-%m-%d')
logger.info(f"开始执行收支记录清零任务,日期: {today}")
# 查询ID=1的收支记录如果不存在则创建
try:
szjilu = Szjilu.objects.get(id=1)
logger.info(f"找到ID=1的收支记录")
except Szjilu.DoesNotExist:
# 创建ID=1的初始记录
szjilu = Szjilu.objects.create(
id=1,
TotalIncome=Decimal('0.00'),
TotalFlow=Decimal('0.00'),
TotalExpense=Decimal('0.00'),
DailyExpense=Decimal('0.00'),
DailyFlow=Decimal('0.00')
)
logger.info(f"创建ID=1的收支记录")
# 记录清零前的值(用于日志)
old_jrzc = szjilu.DailyExpense
old_jrls = szjilu.DailyFlow
# 清零今日字段
szjilu.DailyExpense = Decimal('0.00')
szjilu.DailyFlow = Decimal('0.00')
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
logger.info(f"收支记录清零完成,今日支出原值: {old_jrzc},今日流水原值: {old_jrls}")
result_msg = f"收支记录清零完成 - 日期: {today}, 记录ID: {szjilu.id}"
updated = reset_all_daily_szjilu()
result_msg = f"收支记录清零完成 - 日期: {today}, 俱乐部记录数: {updated}"
log_task_execution("收支记录每日清零", True, result_msg)
return {
"success": True,
"date": today,
"record_id": szjilu.id,
"jrzc_cleared": True,
"jrls_cleared": True,
"old_jrzc": str(old_jrzc),
"old_jrls": str(old_jrls),
"message": result_msg
"clubs_updated": updated,
"message": result_msg,
}
except Exception as e:
@@ -81,5 +39,5 @@ def daily_sz_reset_task():
"success": False,
"error": str(e),
"date": datetime.now().strftime('%Y-%m-%d'),
"message": error_msg
"message": error_msg,
}

View File

@@ -132,20 +132,17 @@ class ShangpinGonggaoView(APIView):
"""
try:
logger.info("收到商品公告和轮播图请求")
# 1. 查询公告类型为1
gonggao_obj = Gonggao.query.filter(NoticeType=1).first()
# 获取公告内容,如果没有则返回空字符串
gonggao_content = gonggao_obj.Content if gonggao_obj else ""
# 2. 查询轮播图类型为1按显示顺序排序
lunbo_queryset = Lunbo.query.filter(ImageType=1)
# 提取轮播图URL列表
lunbo_urls = [lunbo.ImageURL for lunbo in lunbo_queryset]
# 3. 构建返回数据
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
gonggao_content = get_gonggao_content(request, notice_type=1)
lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1)
response_data = {
"shangpingonggao": gonggao_content,
"shangpinlunbo": lunbo_urls
"shangpinlunbo": lunbo_urls,
"page_key": page_key,
}
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)}")
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}")
# 4. 返回响应
return Response(response_data, status=status.HTTP_200_OK)
except Exception as e:
@@ -3158,9 +3155,13 @@ class PopupConfigView(APIView):
'msg': '缺少参数 pageKey'
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查询页面配置
# 3. 查询页面配置(按俱乐部)
from jituan.services.display_config import get_gonggao_content
from jituan.services.club_context import resolve_club_id_from_request
club_id = resolve_club_id_from_request(request)
try:
popup_page = PopupPage.query.get(page_key=page_key, is_active=True)
popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True)
except PopupPage.DoesNotExist:
# 该页面没有配置弹窗,返回空
return Response({
@@ -3206,15 +3207,21 @@ class GetWithdrawModeView(APIView):
permission_classes = [IsAuthenticated] # JWT 认证
def post(self, request, *args, **kwargs):
# 尝试获取 ID=1 的记录
config = WithdrawConfig.query.filter(id=1).first()
# 如果存在则取实际值,否则默认 2手动提现
mode = config.mode if config else 2
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import get_withdraw_mode
user = getattr(request, 'user', None)
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
if not club_id:
club_id = resolve_club_id_from_request(request)
mode = get_withdraw_mode(club_id)
return Response({
'code': 0,
'data': {
'mode': mode
'mode': mode,
'club_id': club_id,
}
})

0
jituan/__init__.py Normal file
View File

7
jituan/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class JituanConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'jituan'
verbose_name = '集团多俱乐部'

27
jituan/constants.py Normal file
View File

@@ -0,0 +1,27 @@
"""集团多俱乐部常量。"""
CLUB_ID_DEFAULT = 'xq'
CLUB_HEADER = 'X-Club-Id'
CLUB_SCOPE_HEADER = 'X-Club-Scope'
DATA_SCOPE_ALL = 'ALL_CLUBS'
DATA_SCOPE_SINGLE = 'SINGLE_CLUB'
# 管理任职角色码(数据范围 / 集团 vs 子公司)
ADMIN_ROLE_LABELS = {
'GROUP_OWNER': '集团总负责人',
'GROUP_SUPER_ADMIN': '集团超管',
'GROUP_AFTER_SALES': '集团售后',
'GROUP_FINANCE': '集团财务',
'GROUP_CONFIG': '集团配置',
'CLUB_OWNER': '俱乐部总负责人',
'CLUB_ADMIN': '俱乐部管理员',
'CLUB_AFTER_SALES': '俱乐部售后',
'CLUB_FINANCE': '俱乐部财务',
'CLUB_OPERATOR': '俱乐部运营',
}
# 功能权限仍走 gvsdsdk Permission.perm_code如 caiwu、dingdan、000001
ADMIN_SCOPE_GROUP = 'GROUP'
ADMIN_SCOPE_CLUB = 'CLUB'

View File

View File

View File

@@ -0,0 +1,47 @@
"""回填 lunbo / gonggao / popup_page / tupianpeizhi 的 club_id 与 lunbo.page_key。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from config.models import Gonggao, Lunbo, PopupPage, Tupianpeizhi
class Command(BaseCommand):
help = '回填展示配置表的 club_id并为 lunbo 补 page_key'
def handle(self, *args, **options):
updated = {'lunbo_club': 0, 'lunbo_page': 0, 'gonggao': 0, 'popup': 0, 'tupian': 0}
with transaction.atomic():
for row in Lunbo.query.all().only('id', 'club_id', 'page_key', 'ImageType'):
fields = []
if not row.club_id:
row.club_id = CLUB_ID_DEFAULT
fields.append('club_id')
updated['lunbo_club'] += 1
if not row.page_key:
if row.ImageType == 2:
row.page_key = 'dashou_center'
else:
row.page_key = 'order_pool'
fields.append('page_key')
updated['lunbo_page'] += 1
if fields:
row.save(update_fields=fields)
for row in Gonggao.query.filter(club_id='').only('id', 'club_id'):
Gonggao.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['gonggao'] += 1
for row in PopupPage.query.filter(club_id='').only('id', 'club_id'):
PopupPage.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['popup'] += 1
for row in Tupianpeizhi.query.filter(club_id='').only('id', 'club_id'):
Tupianpeizhi.query.filter(id=row.id).update(club_id=CLUB_ID_DEFAULT)
updated['tupian'] += 1
self.stdout.write(self.style.SUCCESS(
f'回填完成: lunbo_club={updated["lunbo_club"]}, lunbo_page={updated["lunbo_page"]}, '
f'gonggao={updated["gonggao"]}, popup={updated["popup"]}, tupian={updated["tupian"]}'
))

View File

@@ -0,0 +1,23 @@
"""按充值订单 / 订单 / 用户回填 gsfenhong.club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_gsfenhong_club_id
from products.models import Gsfenhong
class Command(BaseCommand):
help = '回填 gsfenhong.club_id'
def handle(self, *args, **options):
updated = 0
with transaction.atomic():
for row in Gsfenhong.query.all().only('dingdan_id', 'dashouid', 'club_id'):
cid = resolve_gsfenhong_club_id(dingdan_id=row.dingdan_id, dashouid=row.dashouid)
if not cid:
cid = CLUB_ID_DEFAULT
if row.club_id != cid:
Gsfenhong.query.filter(dingdan_id=row.dingdan_id).update(club_id=cid)
updated += 1
self.stdout.write(self.style.SUCCESS(f'gsfenhong club_id 更新 {updated}'))

View File

@@ -0,0 +1,42 @@
"""回填罚单 club_id从关联订单或被罚用户"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_penalty_club_id
from orders.models import Order, Penalty
from users.business_models import User
class Command(BaseCommand):
help = '按关联订单/被罚用户回填 fadan.club_id默认仅处理仍为 xq 的记录)'
def add_arguments(self, parser):
parser.add_argument('--all', action='store_true', help='重算全部罚单(慎用)')
parser.add_argument('--dry-run', action='store_true')
def handle(self, *args, **options):
dry = options['dry_run']
qs = Penalty.query.all()
if not options['all']:
qs = qs.filter(ClubID=CLUB_ID_DEFAULT)
updated = 0
with transaction.atomic():
for p in qs.iterator():
order = None
if p.RelatedOrderID:
order = Order.query.filter(OrderID=p.RelatedOrderID).first()
cid = resolve_penalty_club_id(
order=order,
penalized_user_id=p.PenalizedUserID,
)
if cid and cid != p.ClubID:
if dry:
self.stdout.write(f'would update penalty {p.id}: {p.ClubID} -> {cid}')
else:
p.ClubID = cid
p.save(update_fields=['ClubID'])
updated += 1
self.stdout.write(self.style.SUCCESS(f'罚单 club_id 更新 {updated}'))

View File

@@ -0,0 +1,35 @@
"""回填 chufajilu / duoci_fenhong club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_penalty import resolve_gsfenhong_club_id, resolve_penalty_record_club_id
from orders.models import PenaltyRecord
from products.models import DuociFenhong
class Command(BaseCommand):
help = '回填 PenaltyRecord / DuociFenhong 的 club_id'
def handle(self, *args, **options):
pr_updated = 0
df_updated = 0
with transaction.atomic():
for row in PenaltyRecord.query.all().only('id', 'OrderID', 'PlayerID', 'ClubID'):
cid = resolve_penalty_record_club_id(order_id=row.OrderID, player_id=row.PlayerID)
if row.ClubID != cid:
PenaltyRecord.query.filter(id=row.id).update(ClubID=cid)
pr_updated += 1
for row in DuociFenhong.query.all().only('huiyuan', 'yonghuid', 'cishu', 'club_id'):
cid = resolve_gsfenhong_club_id(dashouid=row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
DuociFenhong.query.filter(
club_id=row.club_id,
huiyuan=row.huiyuan,
yonghuid=row.yonghuid,
cishu=row.cishu,
).update(club_id=cid)
df_updated += 1
self.stdout.write(self.style.SUCCESS(
f'PenaltyRecord={pr_updated}, DuociFenhong={df_updated}'
))

View File

@@ -0,0 +1,36 @@
"""回填角色日统计 club_id按用户归属俱乐部"""
from django.core.management.base import BaseCommand
from django.db import transaction
from backend.models import (
LeaderDailyStats,
ManagerDailyStats,
ManagerRenewalDailyStats,
MerchantDailyStats,
PlayerDailyStats,
)
from backend.utils import _club_id_for_yonghuid
MODELS = [
(MerchantDailyStats, 'MerchantID'),
(PlayerDailyStats, 'PlayerID'),
(LeaderDailyStats, 'LeaderID'),
(ManagerDailyStats, 'ManagerID'),
(ManagerRenewalDailyStats, 'ManagerID'),
]
class Command(BaseCommand):
help = '回填 5 张角色日统计表的 club_id'
def handle(self, *args, **options):
updated = 0
with transaction.atomic():
for model, uid_field in MODELS:
for row in model.query.all().only('id', uid_field, 'club_id'):
cid = _club_id_for_yonghuid(getattr(row, uid_field))
if row.club_id != cid:
model.query.filter(id=row.id).update(club_id=cid)
updated += 1
self.stdout.write(self.style.SUCCESS(f'角色日统计 club_id 更新 {updated}'))

View File

@@ -0,0 +1,42 @@
"""按用户 ClubID 回填提现流水 club_id。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_user import get_user_club_id
from users.business_models import User
from users.models import TixianAutoRecord, Tixianjilu, TixianShenheJilu
class Command(BaseCommand):
help = '回填 tixianjilu / tixian_shenhe_jilu / tixian_auto_record 的 club_id'
def handle(self, *args, **options):
uid_club = {}
for u in User.query.all().only('UserUID', 'ClubID'):
uid_club[u.UserUID] = get_user_club_id(u)
updated = {'jilu': 0, 'shenhe': 0, 'auto': 0}
with transaction.atomic():
for row in Tixianjilu.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
Tixianjilu.query.filter(id=row.id).update(club_id=cid)
updated['jilu'] += 1
for row in TixianShenheJilu.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
TixianShenheJilu.query.filter(id=row.id).update(club_id=cid)
updated['shenhe'] += 1
for row in TixianAutoRecord.query.all().only('id', 'yonghuid', 'club_id'):
cid = uid_club.get(row.yonghuid) or CLUB_ID_DEFAULT
if row.club_id != cid:
TixianAutoRecord.query.filter(id=row.id).update(club_id=cid)
updated['auto'] += 1
self.stdout.write(self.style.SUCCESS(
f'回填完成: jilu={updated["jilu"]}, shenhe={updated["shenhe"]}, auto={updated["auto"]}'
))

View File

@@ -0,0 +1,53 @@
"""回填用户 ClubID缺省或空时设为 xq 或 user_wx_openid 绑定)。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import UserWxOpenid
from users.business_models import User
class Command(BaseCommand):
help = '回填 User.ClubIDOpenID 绑定 → user_wx_openid否则默认 xq'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='仅统计不落库',
)
def handle(self, *args, **options):
dry_run = options.get('dry_run', False)
updated = 0
skipped = 0
users = User.query.all()
for user in users:
current = getattr(user, 'ClubID', None) or ''
if current and current != CLUB_ID_DEFAULT:
skipped += 1
continue
new_club = CLUB_ID_DEFAULT
if user.OpenID:
binding = UserWxOpenid.query.filter(openid=user.OpenID).order_by('id').first()
if binding:
new_club = binding.club_id
if current == new_club:
skipped += 1
continue
if dry_run:
self.stdout.write(f'[dry-run] {user.UserUID} -> ClubID={new_club}')
updated += 1
continue
with transaction.atomic():
user.ClubID = new_club
user.save(update_fields=['ClubID'])
updated += 1
suffix = 'dry-run' if dry_run else ''
self.stdout.write(self.style.SUCCESS(f'完成{suffix}:更新 {updated},跳过 {skipped}'))

View File

@@ -0,0 +1,104 @@
"""从 xq 模板复制配置,创建新子公司俱乐部。"""
from decimal import Decimal
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club, ClubHuiyuanPrice, ClubLilubiao, ClubTixianQuota, ClubWithdrawConfig
from jituan.services.szjilu_accounting import normalize_szjilu_club_id
from jituan.services.display_config import copy_display_config
from config.models import Szjilu
class Command(BaseCommand):
help = '从 xq 复制支付/会员价/利率配置,创建新俱乐部'
def add_arguments(self, parser):
parser.add_argument('club_id', type=str, help='新俱乐部 ID如 xy')
parser.add_argument('--name', type=str, required=True, help='展示名称')
parser.add_argument('--wx-appid', type=str, default='', help='小程序 appid')
parser.add_argument('--from-club', type=str, default=CLUB_ID_DEFAULT, help='模板俱乐部')
def handle(self, *args, **options):
new_id = (options['club_id'] or '').strip()
name = (options['name'] or '').strip()
template_id = normalize_szjilu_club_id(options.get('from_club'))
if not new_id or len(new_id) > 16:
raise CommandError('club_id 无效')
if new_id == template_id:
raise CommandError('新 club_id 不能与模板相同')
try:
template = Club.query.get(club_id=template_id)
except Club.DoesNotExist:
raise CommandError(f'模板俱乐部 {template_id} 不存在,请先 seed_club_xq')
with transaction.atomic():
if Club.query.filter(club_id=new_id).exists():
raise CommandError(f'俱乐部 {new_id} 已存在')
Club.query.create(
club_id=new_id,
name=name,
status=1,
wx_appid=options.get('wx_appid') or template.wx_appid,
wx_secret=template.wx_secret,
mch_id=template.mch_id,
pay_app_id=template.pay_app_id or options.get('wx_appid') or template.wx_appid,
api_v3_key=template.api_v3_key,
cert_serial_no=template.cert_serial_no,
private_key_path=template.private_key_path,
platform_cert_dir=template.platform_cert_dir,
official_appid=template.official_appid,
official_secret=template.official_secret,
official_token=template.official_token,
encoding_aes_key=template.encoding_aes_key,
template_id=template.template_id,
template_max_per_minute=template.template_max_per_minute,
h5_domain=template.h5_domain,
oss_prefix=template.oss_prefix or new_id,
goeasy_appkey=template.goeasy_appkey,
goeasy_secret=template.goeasy_secret,
config_json=dict(template.config_json or {}),
sort_order=template.sort_order + 1,
)
for row in ClubHuiyuanPrice.query.filter(club_id=template_id):
ClubHuiyuanPrice.query.create(
club_id=new_id,
huiyuan_id=row.huiyuan_id,
jiage=row.jiage or Decimal('0'),
guanshifc=row.guanshifc or Decimal('0'),
zuzhangfc=row.zuzhangfc or Decimal('0'),
is_enabled=row.is_enabled,
bankuai_id=row.bankuai_id,
)
for row in ClubLilubiao.query.filter(club_id=template_id):
ClubLilubiao.query.create(
club_id=new_id,
config_type=row.config_type,
lilv=row.lilv or Decimal('0'),
remark=row.remark or '',
)
try:
tpl_wd = ClubWithdrawConfig.query.get(club_id=template_id)
ClubWithdrawConfig.query.create(club_id=new_id, mode=tpl_wd.mode)
except ClubWithdrawConfig.DoesNotExist:
ClubWithdrawConfig.query.create(club_id=new_id, mode=1)
for row in ClubTixianQuota.query.filter(club_id=template_id):
ClubTixianQuota.query.create(
club_id=new_id,
leixing=row.leixing,
daily_limit=row.daily_limit or Decimal('0'),
)
Szjilu.objects.get_or_create(club_id=new_id)
copy_display_config(template_id, new_id)
self.stdout.write(self.style.SUCCESS(f'俱乐部 {new_id} ({name}) 已创建,配置自 {template_id} 复制'))

View File

@@ -0,0 +1,65 @@
"""为后台客服账号初始化 admin_assignment数据范围非功能权限"""
import logging
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_SINGLE, ADMIN_ROLE_LABELS
from jituan.models import AdminAssignment
from users.business_models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '为 kefu 账号创建默认子公司任职club=xq, CLUB_ADMIN超管可不建 assignment'
def add_arguments(self, parser):
parser.add_argument(
'--club-id', default=CLUB_ID_DEFAULT, help='目标俱乐部 ID默认 xq',
)
parser.add_argument(
'--role-code', default='CLUB_ADMIN', help='任职角色码',
)
parser.add_argument(
'--dry-run', action='store_true', help='只打印不写入',
)
def handle(self, *args, **options):
club_id = options['club_id']
role_code = options['role_code']
dry_run = options['dry_run']
kefu_users = User.query.filter(UserType='kefu')
created = 0
skipped = 0
with transaction.atomic():
for u in kefu_users:
if u.IsSuperuser:
skipped += 1
continue
exists = AdminAssignment.query.filter(
yonghuid=u.UserUID, club_id=club_id, role_code=role_code,
).exists()
if exists:
skipped += 1
continue
if dry_run:
self.stdout.write(f'would assign {u.UserUID} -> {club_id} {role_code}')
created += 1
continue
AdminAssignment.query.create(
yonghuid=u.UserUID,
club_id=club_id,
role_code=role_code,
data_scope=DATA_SCOPE_SINGLE,
is_primary=True,
status=1,
)
created += 1
label = ADMIN_ROLE_LABELS.get(role_code, role_code)
self.stdout.write(self.style.SUCCESS(
f'完成:新建 {created} 条,跳过 {skipped}club={club_id}, role={label}'
))

View File

@@ -0,0 +1,183 @@
"""从现网配置初始化 xq 俱乐部及覆盖表。"""
import logging
from decimal import Decimal
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import (
Club,
ClubHuiyuanPrice,
ClubLilubiao,
ClubWithdrawConfig,
UserWxOpenid,
)
from users.business_models import User
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = '初始化星阙(xq)俱乐部主数据,并从 huiyuan/lilubiao 复制覆盖配置'
def handle(self, *args, **options):
with transaction.atomic():
club, created = self._ensure_club_xq()
self.stdout.write(self.style.SUCCESS(
f'club xq: {"新建" if created else "已存在"}'
))
self._copy_huiyuan_prices()
self._copy_lilubiao()
self._ensure_withdraw_config()
migrated = self._migrate_openid_bindings()
self.stdout.write(self.style.SUCCESS(f'user_wx_openid 迁移绑定: {migrated}'))
def _ensure_club_xq(self):
wx_v3 = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {})
defaults = {
'name': '星阙电竞',
'status': 1,
'wx_appid': getattr(settings, 'WEIXIN_APPID', ''),
'wx_secret': getattr(settings, 'WEIXIN_SECRET', ''),
'mch_id': getattr(settings, 'WEIXIN_MCHID', '') or wx_v3.get('MCHID', ''),
'pay_app_id': wx_v3.get('APPID', '') or getattr(settings, 'WEIXIN_APPID', ''),
'api_v3_key': wx_v3.get('API_V3_KEY', ''),
'cert_serial_no': wx_v3.get('CERT_SERIAL_NO', ''),
'private_key_path': wx_v3.get('PRIVATE_KEY_PATH', ''),
'platform_cert_dir': wx_v3.get('PLATFORM_CERT_DIR', ''),
'config_json': {
'mch_key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''),
'transfer_scene_id': wx_v3.get('TRANSFER_SCENE_ID', '1005'),
},
'official_appid': getattr(settings, 'WEIXIN_OFFICIAL_APPID', ''),
'official_secret': getattr(settings, 'WEIXIN_OFFICIAL_SECRET', ''),
'official_token': getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', ''),
'encoding_aes_key': getattr(settings, 'WEIXIN_OFFICIAL_ENCODING_AES_KEY', ''),
'template_id': getattr(settings, 'WEIXIN_TEMPLATE_ID', ''),
'template_max_per_minute': getattr(settings, 'WEIXIN_TEMPLATE_MAX_PER_MINUTE', 950),
'h5_domain': getattr(settings, 'H5_DOMAIN', ''),
'goeasy_appkey': getattr(settings, 'GOEASY_APPKEY', ''),
'goeasy_secret': getattr(settings, 'GOEASY_SECRET', ''),
'sort_order': 0,
}
try:
club = Club.query.get(club_id=CLUB_ID_DEFAULT)
for k, v in defaults.items():
if k == 'config_json':
merged = dict(club.config_json or {})
for ck, cv in v.items():
if cv and not merged.get(ck):
merged[ck] = cv
club.config_json = merged
continue
if v and not getattr(club, k, None):
setattr(club, k, v)
# V3 字段允许用 settings 补全(证书路径等)
for field in ('api_v3_key', 'cert_serial_no', 'private_key_path', 'platform_cert_dir', 'pay_app_id'):
val = defaults.get(field)
if val and not getattr(club, field, None):
setattr(club, field, val)
club.save()
return club, False
except Club.DoesNotExist:
club = Club.query.create(club_id=CLUB_ID_DEFAULT, **defaults)
return club, True
def _copy_huiyuan_prices(self):
from products.models import Huiyuan
count = 0
for h in Huiyuan.query.all():
bankuai_id = getattr(h, 'bankuai_id', None)
existing = ClubHuiyuanPrice.query.filter(
club_id=CLUB_ID_DEFAULT, huiyuan_id=h.huiyuan_id
).first()
if existing:
continue
ClubHuiyuanPrice.query.create(
club_id=CLUB_ID_DEFAULT,
huiyuan_id=h.huiyuan_id,
jiage=h.jiage or Decimal('0'),
guanshifc=h.guanshifc or Decimal('0'),
zuzhangfc=h.zuzhangfc or Decimal('0'),
is_enabled=True,
bankuai_id=bankuai_id,
)
count += 1
self.stdout.write(f'club_huiyuan_price 新增 {count}')
def _copy_lilubiao(self):
from orders.models import CommissionRate
count = 0
for row in CommissionRate.query.all():
config_type = int(row.Platform or 0)
existing = ClubLilubiao.query.filter(
club_id=CLUB_ID_DEFAULT, config_type=config_type
).first()
if existing:
continue
ClubLilubiao.query.create(
club_id=CLUB_ID_DEFAULT,
config_type=config_type,
lilv=row.Rate or Decimal('0'),
remark='',
)
count += 1
self.stdout.write(f'club_lilubiao 新增 {count}')
def _ensure_withdraw_config(self):
from config.models import WithdrawConfig, TixianQuotaDefault
mode = 1
try:
wc = WithdrawConfig.query.first()
if wc:
mode = wc.mode
except Exception:
pass
try:
cfg = ClubWithdrawConfig.query.get(club_id=CLUB_ID_DEFAULT)
cfg.mode = mode
cfg.save(update_fields=['mode'])
except ClubWithdrawConfig.DoesNotExist:
ClubWithdrawConfig.query.create(club_id=CLUB_ID_DEFAULT, mode=mode)
self._copy_tixian_quota()
def _copy_tixian_quota(self):
from config.models import TixianQuotaDefault
from jituan.models import ClubTixianQuota
count = 0
for row in TixianQuotaDefault.query.all():
existing = ClubTixianQuota.query.filter(
club_id=CLUB_ID_DEFAULT, leixing=row.UserType,
).first()
if existing:
continue
ClubTixianQuota.query.create(
club_id=CLUB_ID_DEFAULT,
leixing=row.UserType,
daily_limit=row.default_quota or Decimal('0'),
)
count += 1
self.stdout.write(f'club_tixian_quota 新增 {count}')
def _migrate_openid_bindings(self):
migrated = 0
users = User.query.filter(OpenID__isnull=False).exclude(OpenID='')
for u in users:
exists = UserWxOpenid.query.filter(
club_id=CLUB_ID_DEFAULT, openid=u.OpenID
).exists()
if exists:
continue
UserWxOpenid.query.create(
club_id=CLUB_ID_DEFAULT,
openid=u.OpenID,
yonghuid=u.UserUID,
unionid=u.UnionID,
)
if hasattr(u, 'ClubID') and (not u.ClubID or u.ClubID != CLUB_ID_DEFAULT):
u.ClubID = CLUB_ID_DEFAULT
u.save(update_fields=['ClubID'])
migrated += 1
return migrated

View File

@@ -0,0 +1,40 @@
"""为每个启用俱乐部初始化 szjilu 收支流水行。"""
from django.core.management.base import BaseCommand
from django.db import transaction
from config.models import Szjilu
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
from jituan.services.szjilu_accounting import normalize_szjilu_club_id
class Command(BaseCommand):
help = '为每个俱乐部创建 szjilu 收支流水行(已有则跳过)'
def add_arguments(self, parser):
parser.add_argument(
'--club-id',
type=str,
default='',
help='仅处理指定 club_id默认处理全部启用俱乐部',
)
def handle(self, *args, **options):
club_id = (options.get('club_id') or '').strip()
if club_id:
club_ids = [normalize_szjilu_club_id(club_id)]
else:
club_ids = list(
Club.query.filter(status=1).values_list('club_id', flat=True)
)
if CLUB_ID_DEFAULT not in club_ids:
club_ids.append(CLUB_ID_DEFAULT)
created = 0
with transaction.atomic():
for cid in club_ids:
_, was_created = Szjilu.objects.get_or_create(club_id=cid)
if was_created:
created += 1
self.stdout.write(f'创建 szjilu: club_id={cid}')
self.stdout.write(self.style.SUCCESS(f'完成,新建 {created} 条 szjilu 记录'))

20
jituan/middleware.py Normal file
View File

@@ -0,0 +1,20 @@
"""请求级俱乐部上下文中间件。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
class ClubContextMiddleware:
"""
为每个请求注入 club_id / club_scope。
旧客户端不传 Header 时 club_id 恒为 xq不影响现网。
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.club_id = resolve_club_id_from_request(request)
request.club_scope = resolve_club_scope(request)
if not request.club_id:
request.club_id = CLUB_ID_DEFAULT
return self.get_response(request)

View File

@@ -0,0 +1,183 @@
# Generated for jituan multi-club platform
import gvsdsdk.models
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='Club',
fields=[
('club_id', models.CharField(max_length=16, primary_key=True, serialize=False, verbose_name='俱乐部ID')),
('name', models.CharField(max_length=64, verbose_name='展示名')),
('status', models.IntegerField(default=1, verbose_name='1启用0停用')),
('wx_appid', models.CharField(blank=True, default='', max_length=32)),
('wx_secret', models.CharField(blank=True, default='', max_length=128)),
('mch_id', models.CharField(blank=True, default='', max_length=32)),
('pay_app_id', models.CharField(blank=True, default='', max_length=32)),
('api_v3_key', models.CharField(blank=True, default='', max_length=128)),
('cert_serial_no', models.CharField(blank=True, default='', max_length=64)),
('private_key_path', models.CharField(blank=True, default='', max_length=255)),
('platform_cert_dir', models.CharField(blank=True, default='', max_length=255)),
('official_appid', models.CharField(blank=True, default='', max_length=32)),
('official_secret', models.CharField(blank=True, default='', max_length=128)),
('official_token', models.CharField(blank=True, default='', max_length=64)),
('encoding_aes_key', models.CharField(blank=True, default='', max_length=64)),
('template_id', models.CharField(blank=True, default='', max_length=64)),
('template_max_per_minute', models.IntegerField(default=950)),
('h5_domain', models.CharField(blank=True, default='', max_length=128)),
('oss_prefix', models.CharField(blank=True, default='', max_length=128)),
('goeasy_appkey', models.CharField(blank=True, default='', max_length=64)),
('goeasy_secret', models.CharField(blank=True, default='', max_length=128)),
('config_json', models.JSONField(blank=True, default=dict)),
('company_uuid', models.BinaryField(blank=True, null=True)),
('sort_order', models.IntegerField(default=0)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': '俱乐部',
'db_table': 'club',
},
),
migrations.CreateModel(
name='UserWxOpenid',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('openid', models.CharField(db_index=True, max_length=64)),
('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
('unionid', models.CharField(blank=True, max_length=64, null=True)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'user_wx_openid',
'unique_together': {('club_id', 'openid')},
},
),
migrations.CreateModel(
name='AdminAssignment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('yonghuid', models.CharField(db_index=True, max_length=7)),
('club_id', models.CharField(blank=True, db_index=True, max_length=16, null=True, verbose_name='NULL=集团任职')),
('role_code', models.CharField(default='CLUB_ADMIN', max_length=50)),
('data_scope', models.CharField(default='SINGLE_CLUB', max_length=20)),
('is_primary', models.BooleanField(default=False)),
('granted_by', models.CharField(blank=True, default='', max_length=7)),
('status', models.IntegerField(default=1, verbose_name='1有效0停用')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'admin_assignment',
'unique_together': {('yonghuid', 'club_id', 'role_code')},
},
),
migrations.CreateModel(
name='AdminAuditLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(blank=True, db_index=True, max_length=16, null=True)),
('operator_yonghuid', models.CharField(db_index=True, max_length=7)),
('operator_role_code', models.CharField(blank=True, default='', max_length=50)),
('target_yonghuid', models.CharField(blank=True, max_length=7, null=True)),
('action', models.CharField(db_index=True, max_length=64)),
('resource_type', models.CharField(blank=True, default='', max_length=32)),
('resource_id', models.CharField(blank=True, default='', max_length=64)),
('field_name', models.CharField(blank=True, default='', max_length=64)),
('value_before', models.TextField(blank=True, null=True)),
('value_after', models.TextField(blank=True, null=True)),
('request_ip', models.CharField(blank=True, default='', max_length=64)),
('remark', models.CharField(blank=True, default='', max_length=500)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
],
options={
'db_table': 'admin_audit_log',
'indexes': [models.Index(fields=['club_id', '-CreateTime'], name='admin_audit_club_time_idx')],
},
),
migrations.CreateModel(
name='ClubHuiyuanPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('huiyuan_id', models.CharField(db_index=True, max_length=6)),
('jiage', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('guanshifc', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('zuzhangfc', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('is_enabled', models.BooleanField(default=True)),
('bankuai_id', models.IntegerField(blank=True, null=True)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_huiyuan_price',
'unique_together': {('club_id', 'huiyuan_id')},
},
),
migrations.CreateModel(
name='ClubLilubiao',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('config_type', models.IntegerField(db_index=True, verbose_name='同 lilubiao.fadanpingtai')),
('lilv', models.DecimalField(decimal_places=4, default=0, max_digits=10)),
('remark', models.CharField(blank=True, default='', max_length=255)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_lilubiao',
'unique_together': {('club_id', 'config_type')},
},
),
migrations.CreateModel(
name='ClubWithdrawConfig',
fields=[
('club_id', models.CharField(max_length=16, primary_key=True, serialize=False)),
('mode', models.IntegerField(default=1, verbose_name='1自动2手动')),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_withdraw_config',
},
),
migrations.CreateModel(
name='ClubTixianQuota',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('leixing', models.IntegerField(db_index=True)),
('daily_limit', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_tixian_quota',
'unique_together': {('club_id', 'leixing')},
},
),
migrations.CreateModel(
name='ClubFadanFenhongLilv',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('club_id', models.CharField(db_index=True, max_length=16)),
('shenfen', models.IntegerField(db_index=True)),
('lilv', models.DecimalField(decimal_places=4, default=0, max_digits=10)),
('CreateTime', models.DateTimeField(auto_now_add=True)),
('UpdateTime', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'club_fadan_fenhong_lilv',
'unique_together': {('club_id', 'shenfen')},
},
),
]

View File

157
jituan/models.py Normal file
View File

@@ -0,0 +1,157 @@
"""集团多俱乐部数据模型(新建表,不修改 gvsdsdk"""
import uuid
from decimal import Decimal
from django.db import models
from gvsdsdk.model_base import QModel
class Club(QModel):
"""子公司 / 俱乐部主表。"""
club_id = models.CharField(max_length=16, primary_key=True, verbose_name='俱乐部ID')
name = models.CharField(max_length=64, verbose_name='展示名')
status = models.IntegerField(default=1, verbose_name='1启用0停用')
wx_appid = models.CharField(max_length=32, blank=True, default='')
wx_secret = models.CharField(max_length=128, blank=True, default='')
mch_id = models.CharField(max_length=32, blank=True, default='')
pay_app_id = models.CharField(max_length=32, blank=True, default='')
api_v3_key = models.CharField(max_length=128, blank=True, default='')
cert_serial_no = models.CharField(max_length=64, blank=True, default='')
private_key_path = models.CharField(max_length=255, blank=True, default='')
platform_cert_dir = models.CharField(max_length=255, blank=True, default='')
official_appid = models.CharField(max_length=32, blank=True, default='')
official_secret = models.CharField(max_length=128, blank=True, default='')
official_token = models.CharField(max_length=64, blank=True, default='')
encoding_aes_key = models.CharField(max_length=64, blank=True, default='')
template_id = models.CharField(max_length=64, blank=True, default='')
template_max_per_minute = models.IntegerField(default=950)
h5_domain = models.CharField(max_length=128, blank=True, default='')
oss_prefix = models.CharField(max_length=128, blank=True, default='')
goeasy_appkey = models.CharField(max_length=64, blank=True, default='')
goeasy_secret = models.CharField(max_length=128, blank=True, default='')
config_json = models.JSONField(default=dict, blank=True)
company_uuid = models.BinaryField(max_length=16, null=True, blank=True)
sort_order = models.IntegerField(default=0)
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club'
verbose_name = '俱乐部'
class UserWxOpenid(QModel):
"""微信 openid 与俱乐部用户绑定club_id + openid 唯一)。"""
club_id = models.CharField(max_length=16, db_index=True)
openid = models.CharField(max_length=64, db_index=True)
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
unionid = models.CharField(max_length=64, null=True, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'user_wx_openid'
unique_together = [['club_id', 'openid']]
class AdminAssignment(QModel):
"""管理任职:集团或子公司后台权限范围。"""
yonghuid = models.CharField(max_length=7, db_index=True)
club_id = models.CharField(max_length=16, null=True, blank=True, db_index=True,
verbose_name='NULL=集团任职')
role_code = models.CharField(max_length=50, default='CLUB_ADMIN')
data_scope = models.CharField(max_length=20, default='SINGLE_CLUB')
is_primary = models.BooleanField(default=False)
granted_by = models.CharField(max_length=7, blank=True, default='')
status = models.IntegerField(default=1, verbose_name='1有效0停用')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'admin_assignment'
unique_together = [['yonghuid', 'club_id', 'role_code']]
class AdminAuditLog(QModel):
"""后台敏感操作审计(改余额等)。"""
club_id = models.CharField(max_length=16, null=True, blank=True, db_index=True)
operator_yonghuid = models.CharField(max_length=7, db_index=True)
operator_role_code = models.CharField(max_length=50, blank=True, default='')
target_yonghuid = models.CharField(max_length=7, null=True, blank=True)
action = models.CharField(max_length=64, db_index=True)
resource_type = models.CharField(max_length=32, blank=True, default='')
resource_id = models.CharField(max_length=64, blank=True, default='')
field_name = models.CharField(max_length=64, blank=True, default='')
value_before = models.TextField(null=True, blank=True)
value_after = models.TextField(null=True, blank=True)
request_ip = models.CharField(max_length=64, blank=True, default='')
remark = models.CharField(max_length=500, blank=True, default='')
CreateTime = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'admin_audit_log'
indexes = [models.Index(fields=['club_id', '-CreateTime'])]
class ClubHuiyuanPrice(QModel):
"""各俱乐部会员售价/分成(全局 huiyuan_id 定义不变)。"""
club_id = models.CharField(max_length=16, db_index=True)
huiyuan_id = models.CharField(max_length=6, db_index=True)
jiage = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
guanshifc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
zuzhangfc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
is_enabled = models.BooleanField(default=True)
bankuai_id = models.IntegerField(null=True, blank=True)
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_huiyuan_price'
unique_together = [['club_id', 'huiyuan_id']]
class ClubLilubiao(QModel):
"""各俱乐部分红/手续费配置(对应 lilubiao.fadanpingtai"""
club_id = models.CharField(max_length=16, db_index=True)
config_type = models.IntegerField(db_index=True, verbose_name='同 lilubiao.fadanpingtai')
lilv = models.DecimalField(max_digits=10, decimal_places=4, default=Decimal('0.0000'))
remark = models.CharField(max_length=255, blank=True, default='')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_lilubiao'
unique_together = [['club_id', 'config_type']]
class ClubWithdrawConfig(QModel):
club_id = models.CharField(max_length=16, primary_key=True)
mode = models.IntegerField(default=1, verbose_name='1自动2手动')
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_withdraw_config'
class ClubTixianQuota(QModel):
club_id = models.CharField(max_length=16, db_index=True)
leixing = models.IntegerField(db_index=True)
daily_limit = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_tixian_quota'
unique_together = [['club_id', 'leixing']]
class ClubFadanFenhongLilv(QModel):
club_id = models.CharField(max_length=16, db_index=True)
shenfen = models.IntegerField(db_index=True)
lilv = models.DecimalField(max_digits=10, decimal_places=4, default=Decimal('0.0000'))
CreateTime = models.DateTimeField(auto_now_add=True)
UpdateTime = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'club_fadan_fenhong_lilv'
unique_together = [['club_id', 'shenfen']]

View File

View File

@@ -0,0 +1,114 @@
"""后台操作日志xiugaijilu 按俱乐部过滤 + admin_audit_log 写入。"""
import logging
from jituan.constants import DATA_SCOPE_ALL
from jituan.models import AdminAuditLog
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
from jituan.services.club_user import get_user_club_id
logger = logging.getLogger(__name__)
def club_id_for_yonghuid(yonghuid):
from users.business_models import User
user = User.query.filter(UserUID=str(yonghuid)).first()
return get_user_club_id(user)
def filter_admin_audit_by_request(qs, request):
"""集团 ALL 视图看全部;子公司按 club_id 过滤。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
return qs.filter(club_id=resolve_club_id_from_request(request))
def list_admin_audit_logs(request, page=1, page_size=20, **filters):
"""分页查询 admin_audit_log。"""
qs = AdminAuditLog.query.all().order_by('-CreateTime')
qs = filter_admin_audit_by_request(qs, request)
operator = (filters.get('operator_yonghuid') or '').strip()
target = (filters.get('target_yonghuid') or '').strip()
action = (filters.get('action') or '').strip()
keyword = (filters.get('keyword') or '').strip()
if operator:
qs = qs.filter(operator_yonghuid=operator)
if target:
qs = qs.filter(target_yonghuid=target)
if action:
qs = qs.filter(action=action)
if keyword:
qs = qs.filter(remark__icontains=keyword)
page = max(1, int(page or 1))
page_size = min(100, max(1, int(page_size or 20)))
total = qs.count()
start = (page - 1) * page_size
rows = qs[start:start + page_size]
data_list = []
for r in rows:
data_list.append({
'id': r.id,
'club_id': r.club_id,
'operator_yonghuid': r.operator_yonghuid,
'operator_role_code': r.operator_role_code,
'target_yonghuid': r.target_yonghuid,
'action': r.action,
'resource_type': r.resource_type,
'resource_id': r.resource_id,
'field_name': r.field_name,
'value_before': r.value_before,
'value_after': r.value_after,
'remark': r.remark,
'request_ip': r.request_ip,
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
})
return {
'list': data_list,
'total': total,
'page': page,
'page_size': page_size,
}
def filter_xiugaijilu_by_request(qs, request):
"""集团 ALL 视图看全部;子公司仅看本 club 用户相关记录。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
from users.business_models import User
uids = User.query.filter(ClubID=club_id).values_list('UserUID', flat=True)
uid_list = [str(u) for u in uids]
if not uid_list:
return qs.none()
return qs.filter(yonghuid__in=uid_list)
def log_admin_audit_from_xiugai(
*,
yonghuid,
xiugaiid,
leixing,
qitashuoming,
club_id=None,
request_ip='',
):
"""与 write_xiugai_log 同步写入 admin_audit_log失败不影响主流程"""
try:
cid = club_id or club_id_for_yonghuid(yonghuid)
AdminAuditLog.query.create(
club_id=cid,
operator_yonghuid=str(xiugaiid),
target_yonghuid=str(yonghuid),
action='xiugaijilu',
resource_type='user',
resource_id=str(yonghuid),
field_name=str(leixing),
remark=(qitashuoming or '')[:500],
request_ip=(request_ip or '')[:64],
)
except Exception as e:
logger.warning('写入 admin_audit_log 失败: %s', e)

View File

@@ -0,0 +1,102 @@
"""集团后台管理员俱乐部上下文。"""
from jituan.constants import (
ADMIN_ROLE_LABELS,
DATA_SCOPE_ALL,
DATA_SCOPE_SINGLE,
CLUB_ID_DEFAULT,
)
from jituan.models import AdminAssignment, Club
def build_admin_club_context(user):
"""
根据 admin_assignment + 超管推断后台登录后的俱乐部上下文。
返回 dict 供前端存储与请求头使用。
"""
yonghuid = user.UserUID
is_super = bool(user.IsSuperuser) or user.UserType == 'admin'
assignments = list(
AdminAssignment.query.filter(yonghuid=yonghuid, status=1).order_by('-is_primary', 'club_id')
)
clubs_qs = Club.query.filter(status=1).order_by('sort_order', 'club_id')
all_clubs = [
{'club_id': c.club_id, 'name': c.name}
for c in clubs_qs
]
if is_super and not assignments:
return _pack(
scope=DATA_SCOPE_ALL,
club_id=CLUB_ID_DEFAULT,
is_group_admin=True,
assignments=[],
clubs=all_clubs,
can_switch_club=True,
role_code='GROUP_SUPER_ADMIN',
role_name=ADMIN_ROLE_LABELS['GROUP_SUPER_ADMIN'],
)
if not assignments:
return _pack(
scope=DATA_SCOPE_SINGLE,
club_id=CLUB_ID_DEFAULT,
is_group_admin=False,
assignments=[],
clubs=all_clubs,
can_switch_club=len(all_clubs) > 1,
role_code='CLUB_ADMIN',
role_name='子公司客服(默认)',
)
primary = next((a for a in assignments if a.is_primary), assignments[0])
has_group = any(a.club_id is None or a.data_scope == DATA_SCOPE_ALL for a in assignments)
allowed_club_ids = {a.club_id for a in assignments if a.club_id}
if has_group:
scope = DATA_SCOPE_ALL
club_id = CLUB_ID_DEFAULT
else:
scope = DATA_SCOPE_SINGLE
club_id = primary.club_id or CLUB_ID_DEFAULT
visible_clubs = all_clubs if has_group else [
c for c in all_clubs if c['club_id'] in allowed_club_ids
]
role_code = primary.role_code or 'CLUB_ADMIN'
return _pack(
scope=scope,
club_id=club_id,
is_group_admin=has_group,
assignments=assignments,
clubs=visible_clubs,
can_switch_club=has_group or len(visible_clubs) > 1,
role_code=role_code,
role_name=ADMIN_ROLE_LABELS.get(role_code, role_code),
)
def _pack(scope, club_id, is_group_admin, assignments, clubs, can_switch_club,
role_code, role_name):
return {
'scope': scope,
'club_id': club_id,
'is_group_admin': is_group_admin,
'role_code': role_code,
'role_name': role_name,
'perm_source': 'gvsdsdk',
'perm_note': '功能菜单权限仍由 gvsdsdk UserRole→Permission 控制;本表仅管数据范围',
'assignments': [
{
'club_id': a.club_id,
'role_code': a.role_code,
'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code),
'data_scope': a.data_scope,
'is_primary': a.is_primary,
}
for a in assignments
],
'clubs': clubs,
'can_switch_club': can_switch_club,
}

View File

@@ -0,0 +1,216 @@
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu旧 /houtai/caiwu 不变)。"""
from calendar import monthrange
from datetime import date, datetime, timedelta
from decimal import Decimal
from django.db.models import Sum
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
from jituan.constants import DATA_SCOPE_ALL, CLUB_ID_DEFAULT
from config.models import DailyIncomeStat, DailyPayoutStat, Szjilu
from orders.models import Order
from products.models import Czjilu, Huiyuangoumai
from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang
def _role_profile_qs(profile_model, request):
"""按俱乐部过滤角色扩展表(通过 user.ClubID"""
qs = profile_model.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
return qs.filter(user__ClubID=resolve_club_id_from_request(request))
def _sum_role_balance(profile_model, balance_field, request):
return float(
_role_profile_qs(profile_model, request).aggregate(total=Sum(balance_field))['total'] or 0.00
)
def _build_szjilu_payload(request):
"""全局收支流水表 szjilu按俱乐部或集团汇总"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
agg = Szjilu.query.aggregate(
zongshouyi=Sum('TotalIncome'),
zongliushui=Sum('TotalFlow'),
zongzhichu=Sum('TotalExpense'),
jrls=Sum('DailyFlow'),
jrzc=Sum('DailyExpense'),
)
return {
'zongliushui': float(agg['zongliushui'] or 0),
'zongshouyi': float(agg['zongshouyi'] or 0),
'zongzhichu': float(agg['zongzhichu'] or 0),
'jrls': float(agg['jrls'] or 0),
'jrzc': float(agg['jrzc'] or 0),
}
from jituan.services.szjilu_accounting import get_szjilu_snapshot
return get_szjilu_snapshot(resolve_club_id_from_request(request))
def _order_qs(request):
qs = Order.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(Order, 'ClubID'):
return qs.filter(ClubID=club_id)
return qs
def _czjilu_qs(request):
qs = Czjilu.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(Czjilu, 'club_id'):
return qs.filter(club_id=club_id)
return qs
def _huiyuangoumai_qs(request):
qs = Huiyuangoumai.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(Huiyuangoumai, 'club_id'):
return qs.filter(club_id=club_id)
return qs
def _daily_income_qs(request):
qs = DailyIncomeStat.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(DailyIncomeStat, 'club_id'):
return qs.filter(club_id=club_id)
return qs
def _daily_payout_qs(request):
qs = DailyPayoutStat.query.all()
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(DailyPayoutStat, 'club_id'):
return qs.filter(club_id=club_id)
return qs
def build_caiwu_payload(request):
"""与 CaiwuView 返回结构一致,但按俱乐部过滤订单/充值流水。"""
today = date.today()
today_start = datetime.combine(today, datetime.min.time())
today_end = today_start + timedelta(days=1)
club_id = resolve_club_id_from_request(request)
order_qs = _order_qs(request)
cz_qs = _czjilu_qs(request)
hy_qs = _huiyuangoumai_qs(request)
income_qs = _daily_income_qs(request)
payout_qs = _daily_payout_qs(request)
today_income = income_qs.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 = payout_qs.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 = order_qs.filter(
CreateTime__gte=today_start,
CreateTime__lt=today_end,
Status__in=[1, 2, 3, 4, 5, 6, 7, 8],
)
today_order_count = today_orders.count()
today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00)
today_completed = order_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=3,
)
today_completed_count = today_completed.count()
today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00)
today_tuikuan = order_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=5,
)
today_tuikuan_count = today_tuikuan.count()
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
today_chongzhi = cz_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3,
)
today_chongzhi_count = today_chongzhi.count()
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
new_huiyuan_user_ids = hy_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end,
).values_list('yonghu_id', flat=True).distinct()
today_new_huiyuan = len(list(new_huiyuan_user_ids))
today_new_huiyuan_amount = 0.0
if new_huiyuan_user_ids:
amount_sum = cz_qs.filter(
CreateTime__gte=today_start,
CreateTime__lt=today_end,
yonghuid__in=new_huiyuan_user_ids,
leixing=1,
zhuangtai=3,
).aggregate(total=Sum('jine'))
today_new_huiyuan_amount = float(amount_sum['total'] or 0.0)
all_guanshi_yue = _sum_role_balance(UserGuanshi, 'yue', request)
all_dashou_yue = _sum_role_balance(UserDashou, 'yue', request)
all_zuzhang_yue = _sum_role_balance(UserZuzhang, 'ketixian_jine', request)
all_shangjia_yue = _sum_role_balance(UserShangjia, 'yue', request)
total_income = float(income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00)
total_payout = float(payout_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00)
platform_profit = round(total_income - total_payout, 2)
daily_stats = []
income_list = income_qs.order_by('-date')[:30]
payout_dict = {
str(p.date): float(p.total_amount)
for p in payout_qs.filter(date__gte=today - timedelta(days=30))
}
for inc in income_list:
d = str(inc.date)
daily_stats.append({
'date': d,
'income': float(inc.total_amount),
'income_count': inc.total_count,
'payout': payout_dict.get(d, 0.00),
'profit': round(float(inc.total_amount) - payout_dict.get(d, 0.00), 2),
})
return {
'club_id': club_id,
'scope': resolve_club_scope(request),
'szjilu': _build_szjilu_payload(request),
'today_income': round(today_income_amount, 2),
'today_income_count': today_income_count,
'today_payout': round(today_payout_amount, 2),
'today_payout_count': today_payout_count,
'today_order_count': today_order_count,
'today_order_amount': round(today_order_amount, 2),
'today_completed_count': today_completed_count,
'today_completed_amount': round(today_completed_amount, 2),
'today_tuikuan_count': today_tuikuan_count,
'today_tuikuan_amount': round(today_tuikuan_amount, 2),
'today_chongzhi_count': today_chongzhi_count,
'today_chongzhi_amount': round(today_chongzhi_amount, 2),
'today_new_huiyuan': today_new_huiyuan,
'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2),
'all_guanshi_yue': round(all_guanshi_yue, 2),
'all_dashou_yue': round(all_dashou_yue, 2),
'all_zuzhang_yue': round(all_zuzhang_yue, 2),
'all_shangjia_yue': round(all_shangjia_yue, 2),
'total_income': round(total_income, 2),
'total_payout': round(total_payout, 2),
'platform_profit': platform_profit,
'daily_stats': daily_stats,
}

View File

@@ -0,0 +1,53 @@
"""各俱乐部价格 / 利率覆盖(无覆盖时回落全局 huiyuan / lilubiao"""
from decimal import Decimal
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import ClubHuiyuanPrice, ClubLilubiao
from orders.models import CommissionRate
from products.models import Huiyuan
def get_huiyuan_price(club_id, huiyuan_id, huiyuan_obj=None):
club_id = club_id or CLUB_ID_DEFAULT
row = ClubHuiyuanPrice.query.filter(
club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True,
).first()
if row:
return Decimal(str(row.jiage))
obj = huiyuan_obj or Huiyuan.query.filter(huiyuan_id=huiyuan_id).first()
if obj:
return Decimal(str(obj.jiage))
return Decimal('0')
def get_huiyuan_fenchong(club_id, huiyuan_id, huiyuan_obj=None):
"""返回 (guanshifc, zuzhangfc)。"""
club_id = club_id or CLUB_ID_DEFAULT
row = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id).first()
if row:
return Decimal(str(row.guanshifc)), Decimal(str(row.zuzhangfc))
obj = huiyuan_obj or Huiyuan.query.filter(huiyuan_id=huiyuan_id).first()
if obj:
return Decimal(str(obj.guanshifc)), Decimal(str(getattr(obj, 'zuzhangfc', 0) or 0))
return Decimal('0'), Decimal('0')
def get_commission_rate(club_id, platform_key):
"""
读取分成利率。platform_key 与 lilubiao.Platform 一致(如 '1','3','13')。
"""
club_id = club_id or CLUB_ID_DEFAULT
config_type = int(platform_key or 0)
row = ClubLilubiao.query.filter(club_id=club_id, config_type=config_type).first()
if row is not None:
return Decimal(str(row.lilv))
cr = CommissionRate.query.filter(Platform=str(platform_key)).first()
if cr and cr.Rate is not None:
return Decimal(str(cr.Rate))
return Decimal('0')
def get_commission_rate_object(club_id, platform_key):
"""兼容旧代码 CommissionRate.Rate 访问方式。"""
rate = get_commission_rate(club_id, platform_key)
return type('ClubRate', (), {'Rate': rate, 'Platform': str(platform_key)})()

View File

@@ -0,0 +1,87 @@
"""俱乐部上下文解析与查询辅助。"""
from jituan.constants import (
CLUB_ID_DEFAULT,
CLUB_HEADER,
CLUB_SCOPE_HEADER,
DATA_SCOPE_ALL,
DATA_SCOPE_SINGLE,
)
from jituan.models import Club
def resolve_club_id_from_request(request):
"""
从请求解析 club_id。
优先级:已注入的 request.club_id > Header X-Club-Id > 默认 xq。
旧客户端不传时恒为 xq与现网行为一致。
"""
injected = getattr(request, 'club_id', None)
if injected:
return injected
header = request.META.get(f'HTTP_{CLUB_HEADER.upper().replace("-", "_")}', '')
if not header:
header = request.headers.get(CLUB_HEADER, '') if hasattr(request, 'headers') else ''
club_id = (header or '').strip() or CLUB_ID_DEFAULT
return club_id
def resolve_club_scope(request):
"""解析数据范围ALL_CLUBS集团汇总或 SINGLE_CLUB。"""
scope = getattr(request, 'club_scope', None)
if scope:
return scope
header = request.META.get(f'HTTP_{CLUB_SCOPE_HEADER.upper().replace("-", "_")}', '')
if not header:
header = request.headers.get(CLUB_SCOPE_HEADER, '') if hasattr(request, 'headers') else ''
if (header or '').strip().lower() == 'all':
return DATA_SCOPE_ALL
return DATA_SCOPE_SINGLE
def get_active_club(club_id):
try:
club = Club.query.get(club_id=club_id, status=1)
return club
except Club.DoesNotExist:
return None
def filter_queryset_by_club(qs, request, club_field='club_id'):
"""
按请求上下文过滤 QuerySet / FluentQuery。
集团 ALL_CLUBS 视图不过滤;子公司 SINGLE_CLUB 按 club_id 过滤。
"""
scope = resolve_club_scope(request)
club_id = resolve_club_id_from_request(request)
if scope == DATA_SCOPE_ALL:
return qs
if hasattr(qs, 'filter'):
return qs.filter(**{club_field: club_id})
return qs
def club_id_for_write(request):
"""写入业务流水时使用的 club_id集团汇总视图写入仍须指定目标 club"""
return resolve_club_id_from_request(request)
def orders_for_request(request):
"""按俱乐部过滤的订单 QuerySet。"""
from orders.models import Order
return filter_queryset_by_club(Order.query.all(), request, club_field='ClubID')
def filter_user_related_by_club(qs, request, user_club_path='user__ClubID'):
"""按 User.ClubID 过滤打手/管事/商家等关联查询。"""
scope = resolve_club_scope(request)
if scope == DATA_SCOPE_ALL:
return qs
return qs.filter(**{user_club_path: resolve_club_id_from_request(request)})
def filter_user_qs_by_club(qs, request):
"""直接过滤 User 查询集(字段 ClubID"""
scope = resolve_club_scope(request)
if scope == DATA_SCOPE_ALL:
return qs
return qs.filter(ClubID=resolve_club_id_from_request(request))

View File

@@ -0,0 +1,102 @@
"""罚单 / 管事分红俱乐部隔离与写入。"""
from rest_framework.response import Response
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_context import (
DATA_SCOPE_ALL,
filter_queryset_by_club,
resolve_club_id_from_request,
resolve_club_scope,
)
from jituan.services.club_user import get_user_club_id
def resolve_penalty_club_id(order=None, penalized_user_id=None, request=None):
"""创建罚单时写入 club_id。"""
if order is not None:
cid = getattr(order, 'ClubID', None)
if cid:
return cid
if penalized_user_id:
from users.business_models import User
u = User.query.filter(UserUID=penalized_user_id).first()
if u:
return get_user_club_id(u)
if request is not None:
return resolve_club_id_from_request(request)
return CLUB_ID_DEFAULT
def resolve_gsfenhong_club_id(dingdan_id=None, dashouid=None, order=None, czjilu=None):
"""管事分红写入 club_id。"""
if czjilu is not None and getattr(czjilu, 'club_id', None):
return czjilu.club_id
if order is not None:
cid = getattr(order, 'ClubID', None) or getattr(order, 'club_id', None)
if cid:
return cid
if dingdan_id:
from products.models import Czjilu
cz = Czjilu.query.filter(dingdan_id=dingdan_id).first()
if cz and cz.club_id:
return cz.club_id
from orders.models import Order
o = Order.query.filter(OrderID=dingdan_id).first()
if o and getattr(o, 'ClubID', None):
return o.ClubID
if dashouid:
from users.business_models import User
u = User.query.filter(UserUID=dashouid).first()
if u:
return get_user_club_id(u)
return CLUB_ID_DEFAULT
def filter_penalty_qs(qs, request):
return filter_queryset_by_club(qs, request, club_field='ClubID')
def filter_gsfenhong_qs(qs, request):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
return qs.filter(club_id=club_id)
def forbid_penalty_out_of_scope(request, penalty):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return None
club_id = resolve_club_id_from_request(request)
penalty_club = getattr(penalty, 'ClubID', None) or CLUB_ID_DEFAULT
if penalty_club != club_id:
return Response({'code': 403, 'msg': '该罚单不属于当前俱乐部'})
return None
def resolve_penalty_record_club_id(order_id=None, player_id=None):
if order_id:
from orders.models import Order
o = Order.query.filter(OrderID=order_id).first()
if o and getattr(o, 'ClubID', None):
return o.ClubID
if player_id:
from users.business_models import User
u = User.query.filter(UserUID=player_id).first()
if u:
return get_user_club_id(u)
return CLUB_ID_DEFAULT
def filter_penalty_record_qs(qs, request):
"""积分处罚记录chufajilu按俱乐部过滤。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
if hasattr(qs.model, 'ClubID'):
return qs.filter(ClubID=club_id)
from django.db.models import Q
from orders.models import Order
from users.business_models import User
user_ids = User.query.filter(ClubID=club_id).values_list('UserUID', flat=True)
order_ids = Order.query.filter(ClubID=club_id).values_list('OrderID', flat=True)
return qs.filter(Q(PlayerID__in=user_ids) | Q(OrderID__in=order_ids))

View File

@@ -0,0 +1,79 @@
"""俱乐部微信配置与 openid 解析。"""
import logging
import requests
from django.conf import settings
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
logger = logging.getLogger(__name__)
def resolve_club_by_appid(app_id):
if not app_id:
return None
try:
return Club.query.filter(wx_appid=app_id, status=1).first()
except Exception:
return Club.objects.filter(wx_appid=app_id, status=1).first()
def get_club_wx_credentials(club_id):
"""优先读 club 表xq 缺省时回退 settings 全局配置(兼容现网)。"""
club_id = club_id or CLUB_ID_DEFAULT
try:
club = Club.query.get(club_id=club_id)
except Club.DoesNotExist:
club = None
if club and club.wx_appid and club.wx_secret:
return club.wx_appid, club.wx_secret, club
if club_id == CLUB_ID_DEFAULT:
return (
getattr(settings, 'WEIXIN_APPID', ''),
getattr(settings, 'WEIXIN_SECRET', ''),
club,
)
return '', '', club
def jscode2session(code, club_id=None, app_id=None):
"""
按俱乐部微信配置换取 openid。
club_id 与 app_id 二选一app_id 用于从小程序 appId 反查 club。
"""
club = None
if app_id:
club = resolve_club_by_appid(app_id)
club_id = club.club_id if club else club_id
appid, secret, club = get_club_wx_credentials(club_id)
if not appid or not secret:
return {'errmsg': '俱乐部微信配置未设置'}
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {
'appid': appid,
'secret': secret,
'js_code': code,
'grant_type': 'authorization_code',
}
try:
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'openid' in result:
return {
'openid': result['openid'],
'session_key': result.get('session_key', ''),
'unionid': result.get('unionid', ''),
'club_id': club_id or CLUB_ID_DEFAULT,
'wx_appid': appid,
}
errcode = result.get('errcode', 'unknown')
errmsg = result.get('errmsg', '未知错误')
return {'errmsg': f'[{errcode}]{errmsg}'}
except requests.exceptions.Timeout:
return {'errmsg': '请求微信接口超时'}
except Exception as e:
logger.exception('jscode2session 异常 club_id=%s', club_id)
return {'errmsg': f'请求微信接口异常: {str(e)}'}

View File

@@ -0,0 +1,34 @@
"""用户所属俱乐部解析。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import UserWxOpenid
def get_user_club_id(user):
"""
解析用户归属 club注册/邀请链隔离用)。
优先级User.ClubID → user_wx_openid → 默认 xq。
"""
if not user:
return CLUB_ID_DEFAULT
club_id = getattr(user, 'ClubID', None) or getattr(user, 'club_id', None)
if club_id:
return club_id
uid = getattr(user, 'UserUID', None) or getattr(user, 'yonghuid', None)
if uid:
binding = UserWxOpenid.query.filter(yonghuid=uid).order_by('id').first()
if binding:
return binding.club_id
return CLUB_ID_DEFAULT
def ensure_user_club_id(user, club_id):
"""登录/注册后确保 User.ClubID 与绑定 club 一致。"""
if not user or not club_id:
return
current = getattr(user, 'ClubID', None)
if current != club_id and hasattr(user, 'ClubID'):
user.ClubID = club_id
user.save(update_fields=['ClubID'])

View File

@@ -0,0 +1,27 @@
"""后台按俱乐部校验用户是否在当前数据范围内。"""
from rest_framework.response import Response
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
def user_belongs_to_request(user, request):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return True
club_id = resolve_club_id_from_request(request)
user_club = getattr(user, 'ClubID', None) or ''
return user_club == club_id
def forbid_if_user_out_of_scope(request, user):
"""不属于当前俱乐部时返回 Response否则返回 None。"""
if user_belongs_to_request(user, request):
return None
return Response({'code': 403, 'msg': '该用户不属于当前俱乐部数据范围'})
def list_response_meta(request):
return {
'club_id': resolve_club_id_from_request(request),
'scope': resolve_club_scope(request),
}

View File

@@ -0,0 +1,18 @@
"""业务流水写入时的 club_id 解析。"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.club_context import club_id_for_write
from jituan.services.club_user import get_user_club_id
def resolve_club_id_for_write(request=None, user=None, club_id=None):
"""
写入订单/充值/购买记录时的 club_id。
优先级:显式 club_id > request 头 > 用户归属 > xq。
"""
if club_id:
return club_id
if request is not None:
return club_id_for_write(request)
if user is not None:
return get_user_club_id(user)
return CLUB_ID_DEFAULT

View File

@@ -0,0 +1,129 @@
"""轮播 / 公告 / 弹窗按俱乐部读取与写入。"""
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.services.club_context import filter_queryset_by_club, resolve_club_id_from_request, resolve_club_scope
from rest_framework.response import Response
# 定稿 B 方案 page_key
LUNBO_PAGE_ORDER_POOL = 'order_pool'
LUNBO_PAGE_ACCEPT_ORDER = 'accept_order'
LUNBO_PAGE_MERCHANT_HOME = 'merchant_home'
LUNBO_PAGE_DASHOU_CENTER = 'dashou_center'
LUNBO_PAGE_OPTIONS = [
(LUNBO_PAGE_ORDER_POOL, '抢单池'),
(LUNBO_PAGE_ACCEPT_ORDER, '点单页'),
(LUNBO_PAGE_MERCHANT_HOME, '商家首页'),
(LUNBO_PAGE_DASHOU_CENTER, '打手个人中心背景'),
]
def normalize_page_key(page_key, image_type=1):
key = (page_key or '').strip()
if image_type == 2:
return LUNBO_PAGE_DASHOU_CENTER
if key in {LUNBO_PAGE_ORDER_POOL, LUNBO_PAGE_ACCEPT_ORDER, LUNBO_PAGE_MERCHANT_HOME}:
return key
return LUNBO_PAGE_ORDER_POOL
def forbid_display_write_in_all_scope(request):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 403, 'msg': '请在子公司视图下修改展示配置'})
return None
def filter_popup_page_by_request(qs, request):
return filter_queryset_by_club(qs, request, club_field='club_id')
def forbid_popup_page_out_of_scope(request, page):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return None
club_id = resolve_club_id_from_request(request)
page_club = getattr(page, 'club_id', None) or CLUB_ID_DEFAULT
if page_club != club_id:
return Response({'code': 403, 'msg': '该弹窗配置不属于当前俱乐部'})
return None
def get_gonggao_content(request, notice_type=1):
from config.models import Gonggao
club_id = resolve_club_id_from_request(request)
obj = Gonggao.query.filter(club_id=club_id, NoticeType=notice_type).first()
return obj.Content if obj and obj.Content else ''
def get_lunbo_urls(request, page_key=None, image_type=1):
from config.models import Lunbo
club_id = resolve_club_id_from_request(request)
pk = normalize_page_key(page_key, image_type=image_type)
qs = Lunbo.query.filter(club_id=club_id, ImageType=image_type, page_key=pk)
return [row.ImageURL for row in qs.order_by('id') if row.ImageURL]
def list_response_meta(request):
from jituan.services.club_user_access import list_response_meta as _meta
return _meta(request)
def copy_display_config(template_club_id, new_club_id):
"""从模板俱乐部复制轮播/公告/弹窗/图片配置到新俱乐部。"""
from config.models import Gonggao, Lunbo, PopupConfig, PopupImage, PopupPage, Tupianpeizhi
for row in Lunbo.query.filter(club_id=template_club_id):
Lunbo.query.create(
club_id=new_club_id,
page_key=row.page_key or 'order_pool',
ImageURL=row.ImageURL,
ImageType=row.ImageType,
)
for row in Gonggao.query.filter(club_id=template_club_id):
Gonggao.query.create(
club_id=new_club_id,
NoticeType=row.NoticeType,
Content=row.Content,
)
for row in Tupianpeizhi.query.filter(club_id=template_club_id):
if Tupianpeizhi.query.filter(club_id=new_club_id, ImageType=row.ImageType).exists():
continue
Tupianpeizhi.query.create(
club_id=new_club_id,
ImageURL=row.ImageURL,
ImageType=row.ImageType,
AccessCount=0,
)
for page in PopupPage.query.filter(club_id=template_club_id).prefetch_related('popups__images'):
new_page = PopupPage.query.create(
club_id=new_club_id,
page_key=page.page_key,
name=page.name,
description=page.description,
is_active=page.is_active,
ignore_user_mute=page.ignore_user_mute,
)
for popup in page.popups.all():
new_popup = PopupConfig.query.create(
page=new_page,
popup_id=popup.popup_id,
title=popup.title,
description=popup.description,
strategy_type=popup.strategy_type,
max_count=popup.max_count,
reset_interval=popup.reset_interval,
duration=popup.duration,
force_even_muted=popup.force_even_muted,
sort_order=popup.sort_order,
is_active=popup.is_active,
start_time=popup.start_time,
end_time=popup.end_time,
)
for img in popup.images.all():
PopupImage.query.create(
popup_config=new_popup,
image_url=img.image_url,
text=img.text,
sort_order=img.sort_order,
)

View File

@@ -0,0 +1,492 @@
"""财务子模块统计(订单/会员/其他充值),供 /jituan/houtai/*;旧 /houtai/* 不变。"""
from datetime import date, datetime
from django.db.models import Count, Q, Sum
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from jituan.constants import DATA_SCOPE_ALL
from jituan.models import ClubHuiyuanPrice
from jituan.services.club_context import (
filter_queryset_by_club,
resolve_club_id_from_request,
resolve_club_scope,
)
from jituan.services.club_penalty import filter_gsfenhong_qs, filter_penalty_qs
from orders.models import Order, Penalty
from products.models import Bankuai, Czjilu, Gsfenhong, Huiyuan, ShangpinLeixing
def _meta(request):
return {
'club_id': resolve_club_id_from_request(request),
'scope': resolve_club_scope(request),
}
def _parse_time_range(granularity, year, month, summary_date):
now = date.today()
if granularity == 'day':
if not year or not month:
return None, '按日统计需要年份和月份'
start_date = date(int(year), int(month), 1)
end_date = date(int(year), int(month) + 1, 1) if int(month) < 12 else date(int(year) + 1, 1, 1)
trunc_func = TruncDate('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m-%d')
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
elif granularity == 'month':
if not year:
return None, '按月统计需要年份'
y = int(year)
start_date = date(y, 1, 1)
end_date = date(y + 1, 1, 1)
trunc_func = TruncMonth('CreateTime')
if not summary_date:
summary_date = now.strftime('%Y-%m')
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
elif granularity == 'year':
y = int(year or now.year)
start_date = date(y, 1, 1)
end_date = date(y + 1, 1, 1)
trunc_func = TruncYear('CreateTime')
if not summary_date:
summary_date = str(y)
summary_q = date(y, 1, 1)
else:
return None, '无效的颗粒度'
start_dt = datetime.combine(start_date, datetime.min.time())
end_dt = datetime.combine(end_date, datetime.min.time())
return {
'start_dt': start_dt,
'end_dt': end_dt,
'trunc_func': trunc_func,
'summary_q': summary_q,
'granularity': granularity,
'year': int(year) if year is not None else None,
'month': int(month) if month is not None else None,
}, None
def _summary_date_filter(granularity, summary_q, field_prefix='CreateTime'):
if granularity == 'day':
return {f'{field_prefix}__date': summary_q}
if granularity == 'month':
return {f'{field_prefix}__year': summary_q.year, f'{field_prefix}__month': summary_q.month}
return {f'{field_prefix}__year': summary_q.year}
def build_order_types_payload():
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
return list(types)
def build_order_finance_payload(
request,
granularity,
year,
month,
type_id=0,
order_source=0,
summary_date=None,
):
parsed, err = _parse_time_range(granularity, year, month, summary_date)
if err:
return None, err
start_dt = parsed['start_dt']
end_dt = parsed['end_dt']
trunc_func = parsed['trunc_func']
summary_q = parsed['summary_q']
granularity = parsed['granularity']
filters = {
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt,
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8],
}
if int(type_id or 0) != 0:
filters['ProductTypeID'] = type_id
if int(order_source or 0) in [1, 2]:
filters['Platform'] = int(order_source)
order_base = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
qs = order_base.annotate(period=trunc_func).values('period').annotate(
order_count=Count('id'),
order_amount=Sum('Amount'),
success_count=Count('id', filter=Q(Status=3)),
success_amount=Sum('Amount', filter=Q(Status=3)),
refund_count=Count('id', filter=Q(Status=5)),
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
).order_by('period')
time_series = []
for item in qs:
period_str = (
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
else item['period'].strftime('%Y-%m') if granularity == 'month'
else item['period'].strftime('%Y')
)
platform_profit = (item['platform_success_amount'] or 0) - (item['platform_dashou'] or 0) - (item['dianpu_fenhong_sum'] or 0)
merchant_profit = (item['merchant_success_amount'] or 0) - (item['merchant_dashou'] or 0)
time_series.append({
'date': period_str,
'order_count': item['order_count'],
'order_amount': float(item['order_amount'] or 0),
'success_count': item['success_count'],
'success_amount': float(item['success_amount'] or 0),
'refund_count': item['refund_count'],
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
'profit': round(float(platform_profit + merchant_profit), 2),
})
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
total_order_count = summary_qs.count()
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
total_success_count = summary_qs.filter(Status=3).count()
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
total_refund_count = summary_qs.filter(Status=5).count()
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
s=Sum('pingtai_kuozhan__ShopIncome'),
)['s'] or 0
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
profit_merchant = merchant_amount - merchant_dashou_sum
total_profit = profit_platform + profit_merchant
data = {
**_meta(request),
'time_series': time_series,
'summary': {
'total_order_count': total_order_count,
'total_order_amount': round(float(total_order_amount), 2),
'total_success_count': total_success_count,
'total_success_amount': round(float(total_success_amount), 2),
'total_refund_count': total_refund_count,
'total_PlayerCommission': round(float(total_dashou), 2),
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
'total_profit': round(float(total_profit), 2),
},
}
return data, None
def build_chongzhi_finance_payload(request, granularity, year, month, types=None, summary_date=None):
if types is None:
types = [2, 3, 4, 5, 6]
if isinstance(types, list) and len(types) == 0:
types = [2, 3, 4, 5, 6]
parsed, err = _parse_time_range(granularity, year, month, summary_date)
if err:
return None, err
start_dt = parsed['start_dt']
end_dt = parsed['end_dt']
trunc_func = parsed['trunc_func']
summary_q = parsed['summary_q']
granularity = parsed['granularity']
time_series_map = {}
def ensure_date(ds):
if ds not in time_series_map:
time_series_map[ds] = {}
return time_series_map[ds]
cz_types = [t for t in types if t in [2, 3, 4, 5]]
if cz_types:
cz_qs = filter_queryset_by_club(
Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3,
),
request,
).annotate(period=trunc_func).values('period', 'leixing').annotate(
total_count=Count('id'),
total_amount=Sum('jine'),
).order_by('period', 'leixing')
for item in cz_qs:
period_str = (
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
else item['period'].strftime('%Y-%m') if granularity == 'month'
else item['period'].strftime('%Y')
)
lx = item['leixing']
amount = float(item['total_amount'] or 0)
count = item['total_count']
profit = amount if lx == 3 else 0.0
day_data = ensure_date(period_str)
day_data[str(lx)] = {
'count': count,
'amount': amount,
'profit': profit,
'fenhong_count': 0,
'fenhong_amount': 0.0,
}
if 6 in types:
penalty_qs = filter_penalty_qs(
Penalty.query.filter(CreateTime__gte=start_dt, CreateTime__lt=end_dt, Status=2),
request,
).annotate(period=trunc_func).values('period').annotate(
penalty_count=Count('id'),
penalty_amount=Sum('FineAmount'),
fenhong_amount=Sum('ApplicantBonusAmount'),
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)),
).order_by('period')
for item in penalty_qs:
period_str = (
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
else item['period'].strftime('%Y-%m') if granularity == 'month'
else item['period'].strftime('%Y')
)
day_data = ensure_date(period_str)
penalty_amt = float(item['penalty_amount'] or 0)
fenhong_amt = float(item['fenhong_amount'] or 0)
day_data['6'] = {
'count': item['penalty_count'],
'amount': penalty_amt,
'profit': round(penalty_amt - fenhong_amt, 2),
'fenhong_count': item['fenhong_count'],
'fenhong_amount': fenhong_amt,
}
time_series = []
for period_str in sorted(time_series_map.keys()):
entry = {'date': period_str, 'types': {}}
for type_code in sorted(time_series_map[period_str].keys()):
entry['types'][type_code] = time_series_map[period_str][type_code]
time_series.append(entry)
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
for lx in cz_types:
filter_kwargs = {'leixing': lx, 'zhuangtai': 3}
filter_kwargs.update(_summary_date_filter(granularity, summary_q))
agg = filter_queryset_by_club(Czjilu.query.filter(**filter_kwargs), request).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
summary_result['by_type'][str(lx)] = {
'count': count,
'amount': round(amount, 2),
'profit': round(profit, 2),
'fenhong_count': 0,
'fenhong_amount': 0.0,
}
summary_result['total_count'] += count
summary_result['total_amount'] += amount
summary_result['total_profit'] += profit
if 6 in types:
penalty_filter = {'Status': 2}
penalty_filter.update(_summary_date_filter(granularity, summary_q))
agg = filter_penalty_qs(Penalty.query.filter(**penalty_filter), request).aggregate(
count=Count('id'),
amount=Sum('FineAmount'),
fenhong_amount=Sum('ApplicantBonusAmount'),
fenhong_count=Count('id', filter=Q(ApplicantBonusAmount__gt=0)),
)
penalty_count = agg['count'] or 0
penalty_amount = float(agg['amount'] or 0)
fh_amount = float(agg['fenhong_amount'] or 0)
fh_count = agg['fenhong_count'] or 0
profit = round(penalty_amount - fh_amount, 2)
summary_result['by_type']['6'] = {
'count': penalty_count,
'amount': round(penalty_amount, 2),
'profit': profit,
'fenhong_count': fh_count,
'fenhong_amount': round(fh_amount, 2),
}
summary_result['total_count'] += penalty_count
summary_result['total_amount'] += penalty_amount
summary_result['total_profit'] += profit
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
return {
**_meta(request),
'time_series': time_series,
'summary': summary_result,
}, None
def build_huiyuan_bankuai_payload(request):
club_id = resolve_club_id_from_request(request)
price_map = {}
if resolve_club_scope(request) != DATA_SCOPE_ALL:
for row in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True):
price_map[row.huiyuan_id] = {
'jiage': float(row.jiage or 0),
'guanshifc': float(row.guanshifc or 0),
'zuzhangfc': float(row.zuzhangfc or 0),
}
bankuai_list = []
for bk in Bankuai.query.all():
members = []
for m in Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id'):
item = dict(m)
if m['huiyuan_id'] in price_map:
item['club_price'] = price_map[m['huiyuan_id']]
members.append(item)
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'members': members,
})
return {**_meta(request), 'bankuai_list': bankuai_list}
def build_huiyuan_stats_payload(
request,
huiyuan_id,
granularity,
year,
month,
include_guanshi=False,
include_zuzhang=False,
summary_date=None,
):
if not huiyuan_id:
return None, '缺少会员ID'
parsed, err = _parse_time_range(granularity, year, month, summary_date)
if err:
return None, err
start_dt = parsed['start_dt']
end_dt = parsed['end_dt']
trunc_func = parsed['trunc_func']
summary_q = parsed['summary_q']
granularity = parsed['granularity']
chongzhi_filter = {
'huiyuan_id': huiyuan_id,
'leixing': 1,
'zhuangtai': 3,
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt,
}
fenhong_filter = {
'huiyuan_id': huiyuan_id,
'CreateTime__gte': start_dt,
'CreateTime__lt': end_dt,
}
chongzhi_qs = filter_queryset_by_club(
Czjilu.query.filter(**chongzhi_filter), request,
).annotate(period=trunc_func).values('period').annotate(
chongzhi_count=Count('id'),
chongzhi_amount=Sum('jine'),
).order_by('period')
fenhong_qs = None
if include_guanshi or include_zuzhang:
fenhong_agg = {}
if include_guanshi:
fenhong_agg['guanshi_amount'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
fenhong_qs = filter_gsfenhong_qs(
Gsfenhong.query.filter(**fenhong_filter), request,
).annotate(period=trunc_func).values('period').annotate(**fenhong_agg).order_by('period')
data_map = {}
for item in chongzhi_qs:
period_str = (
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
else item['period'].strftime('%Y-%m') if granularity == 'month'
else item['period'].strftime('%Y')
)
data_map[period_str] = {
'chongzhi_count': item['chongzhi_count'],
'chongzhi_amount': float(item['chongzhi_amount'] or 0),
}
if fenhong_qs:
for item in fenhong_qs:
period_str = (
item['period'].strftime('%Y-%m-%d') if granularity == 'day'
else item['period'].strftime('%Y-%m') if granularity == 'month'
else item['period'].strftime('%Y')
)
if period_str not in data_map:
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
if include_guanshi:
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
if include_zuzhang:
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
time_series = []
for period_str, vals in data_map.items():
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
vals['date'] = period_str
time_series.append(vals)
time_series.sort(key=lambda x: x['date'])
summary_chongzhi_filter = dict(chongzhi_filter)
summary_chongzhi_filter.pop('CreateTime__gte', None)
summary_chongzhi_filter.pop('CreateTime__lt', None)
summary_chongzhi_filter.update(_summary_date_filter(granularity, summary_q))
summary_chongzhi = filter_queryset_by_club(
Czjilu.query.filter(**summary_chongzhi_filter), request,
).aggregate(total_count=Count('id'), total_amount=Sum('jine'))
total_chongzhi_count = summary_chongzhi['total_count'] or 0
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
total_guanshi = 0.0
total_zuzhang = 0.0
if include_guanshi or include_zuzhang:
summary_fenhong_filter = dict(fenhong_filter)
summary_fenhong_filter.pop('CreateTime__gte', None)
summary_fenhong_filter.pop('CreateTime__lt', None)
summary_fenhong_filter.update(_summary_date_filter(granularity, summary_q))
fenhong_agg = {}
if include_guanshi:
fenhong_agg['guanshi_sum'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
summary_fenhong = filter_gsfenhong_qs(
Gsfenhong.query.filter(**summary_fenhong_filter), request,
).aggregate(**fenhong_agg)
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
return {
**_meta(request),
'time_series': time_series,
'summary': {
'total_chongzhi_count': total_chongzhi_count,
'total_chongzhi_amount': total_chongzhi_amount,
'total_guanshi_fenhong': total_guanshi,
'total_zuzhang_fenhong': total_zuzhang,
'total_shouyi': total_shouyi,
},
}, None

View File

@@ -0,0 +1,24 @@
"""邀请码 / 邀请链俱乐部隔离。"""
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_user_club_id
def assert_invite_same_club(request, inviter_user, invitee_user=None):
"""
校验邀请人与被邀请人属于同一俱乐部,且与请求 club 一致。
现网全部 xq 时行为与改造前一致。
返回 (ok, message)
"""
club_id = resolve_club_id_from_request(request)
inviter_club = get_user_club_id(inviter_user)
if inviter_club != club_id:
return False, '邀请码不属于当前小程序俱乐部'
if invitee_user is not None:
invitee_club = get_user_club_id(invitee_user)
if invitee_club != club_id:
return False, '您的账号归属与当前小程序不一致,请使用对应俱乐部小程序'
if invitee_club != inviter_club:
return False, '邀请码与您的俱乐部不匹配'
return True, ''

View File

@@ -0,0 +1,24 @@
"""考核模块按俱乐部过滤。"""
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
from users.business_models import User
def filter_shenhe_jilu_by_request(qs, request):
"""考核记录:按申请人(打手)所属俱乐部过滤。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return qs
club_id = resolve_club_id_from_request(request)
club_uids = list(
User.query.filter(ClubID=club_id).values_list('UserUID', flat=True)
)
if not club_uids:
return qs.none()
return qs.filter(shenqingren_id__in=club_uids)
def yonghu_chenghao_qs_for_request(base_qs, request):
"""板块标签用户数统计:按俱乐部过滤打手。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return base_qs
return base_qs.filter(yonghu__ClubID=resolve_club_id_from_request(request))

View File

@@ -0,0 +1,84 @@
"""各俱乐部全局收支流水szjilu统一记账。"""
from decimal import Decimal
from django.db import transaction
from config.models import Szjilu
from jituan.constants import CLUB_ID_DEFAULT
_ZERO = Decimal('0.00')
_SZJILU_DEFAULTS = {
'TotalIncome': _ZERO,
'TotalFlow': _ZERO,
'TotalExpense': _ZERO,
'DailyExpense': _ZERO,
'DailyFlow': _ZERO,
}
def normalize_szjilu_club_id(club_id):
return (club_id or '').strip() or CLUB_ID_DEFAULT
def _get_szjilu_for_update(club_id):
cid = normalize_szjilu_club_id(club_id)
szjilu, _ = Szjilu.objects.select_for_update().get_or_create(
club_id=cid,
defaults=dict(_SZJILU_DEFAULTS),
)
return szjilu
def apply_szjilu_income(amount, club_id=None):
"""平台收入:总收益、总流水、今日流水增加。"""
jine = Decimal(str(amount))
with transaction.atomic():
szjilu = _get_szjilu_for_update(club_id)
szjilu.TotalIncome += jine
szjilu.TotalFlow += jine
szjilu.DailyFlow += jine
szjilu.save()
def apply_szjilu_expense(amount, club_id=None):
"""平台出款/支出:总收益减少,总支出、今日支出增加。"""
jine = Decimal(str(amount))
with transaction.atomic():
szjilu = _get_szjilu_for_update(club_id)
szjilu.TotalIncome -= jine
szjilu.TotalExpense += jine
szjilu.DailyExpense += jine
szjilu.save()
def get_szjilu_snapshot(club_id=None):
"""读取某俱乐部收支汇总;无记录时返回零值。"""
cid = normalize_szjilu_club_id(club_id)
row = Szjilu.query.filter(club_id=cid).first()
if not row:
return {
'zongliushui': 0.0,
'zongshouyi': 0.0,
'zongzhichu': 0.0,
'jrls': 0.0,
'jrzc': 0.0,
}
return {
'zongliushui': float(row.TotalFlow or _ZERO),
'zongshouyi': float(row.TotalIncome or _ZERO),
'zongzhichu': float(row.TotalExpense or _ZERO),
'jrls': float(row.DailyFlow or _ZERO),
'jrzc': float(row.DailyExpense or _ZERO),
}
def reset_all_daily_szjilu():
"""每日清零:所有俱乐部的今日流水/今日支出。"""
updated = 0
with transaction.atomic():
for szjilu in Szjilu.objects.select_for_update().all():
szjilu.DailyExpense = _ZERO
szjilu.DailyFlow = _ZERO
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
updated += 1
return updated

View File

@@ -0,0 +1,119 @@
"""每日收支详细统计(供 /jituan/houtai/szxx旧 /houtai/szxx 不变)。"""
from datetime import date, datetime
from django.db.models import Sum
from config.models import DailyIncomeStat, DailyPayoutStat
from jituan.services.club_context import filter_queryset_by_club, resolve_club_id_from_request, resolve_club_scope
def build_szxx_payload(request, granularity, year, month, summary_date):
"""
与 SzxxView 返回结构一致,按俱乐部过滤日收入/日出款统计。
返回 (data_dict, error_msg)error_msg 非空时表示参数错误。
"""
now = date.today()
if granularity == 'day':
if not year or not month:
return None, '按日统计需要年份和月份'
group_field = 'day'
income_filter = {'year': year, 'month': month}
payout_filter = {'year': year, 'month': month}
if not summary_date:
summary_date = now.strftime('%Y-%m-%d')
summary_parts = summary_date.split('-')
summary_filter = {
'year': int(summary_parts[0]),
'month': int(summary_parts[1]),
'day': int(summary_parts[2]),
}
elif granularity == 'month':
if not year:
return None, '按月统计需要年份'
group_field = 'month'
income_filter = {'year': year}
payout_filter = {'year': year}
if not summary_date:
summary_date = now.strftime('%Y-%m')
summary_parts = summary_date.split('-')
summary_filter = {'year': int(summary_parts[0]), 'month': int(summary_parts[1])}
elif granularity == 'year':
group_field = 'year'
income_filter = {}
payout_filter = {}
if not summary_date:
summary_date = str(now.year)
summary_filter = {'year': int(summary_date)}
else:
return None, '无效的颗粒度'
income_qs = filter_queryset_by_club(
DailyIncomeStat.query.filter(**income_filter), request,
).values(group_field).annotate(
total_income=Sum('total_amount'),
total_count=Sum('total_count'),
).order_by(group_field)
payout_qs = filter_queryset_by_club(
DailyPayoutStat.query.filter(**payout_filter), request,
).values(group_field).annotate(
total_payout=Sum('total_amount'),
total_count=Sum('total_count'),
).order_by(group_field)
data_map = {}
for item in income_qs:
key = item[group_field]
data_map[key] = {
'income': float(item['total_income'] or 0),
'income_count': item['total_count'] or 0,
'payout': 0.0,
'payout_count': 0,
}
for item in payout_qs:
key = item[group_field]
if key not in data_map:
data_map[key] = {'income': 0.0, 'income_count': 0, 'payout': 0.0, 'payout_count': 0}
data_map[key]['payout'] = float(item['total_payout'] or 0)
data_map[key]['payout_count'] = item['total_count'] or 0
time_series = []
for key, vals in data_map.items():
if granularity == 'day':
date_str = f"{year}-{month:02d}-{key:02d}"
elif granularity == 'month':
date_str = f"{year}-{key:02d}"
else:
date_str = str(key)
profit = round(vals['income'] - vals['payout'], 2)
time_series.append({
'date': date_str,
'income': vals['income'],
'payout': vals['payout'],
'profit': profit,
})
time_series.sort(key=lambda x: x['date'])
income_summary = filter_queryset_by_club(
DailyIncomeStat.query.filter(**summary_filter), request,
).aggregate(total_income=Sum('total_amount'), total_count=Sum('total_count'))
payout_summary = filter_queryset_by_club(
DailyPayoutStat.query.filter(**summary_filter), request,
).aggregate(total_payout=Sum('total_amount'), total_count=Sum('total_count'))
total_income = float(income_summary['total_income'] or 0)
total_payout = float(payout_summary['total_payout'] or 0)
return {
'club_id': resolve_club_id_from_request(request),
'scope': resolve_club_scope(request),
'time_series': time_series,
'summary': {
'total_income': total_income,
'total_income_count': income_summary['total_count'] or 0,
'total_payout': total_payout,
'total_payout_count': payout_summary['total_count'] or 0,
'total_profit': round(total_income - total_payout, 2),
},
}, None

View File

@@ -0,0 +1,32 @@
"""提现流水按俱乐部隔离。"""
from rest_framework.response import Response
from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import filter_queryset_by_club, resolve_club_id_from_request, resolve_club_scope
from jituan.services.club_user import get_user_club_id
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
def club_id_for_tixian_user(user):
return get_user_club_id(user)
def club_id_for_tixian_yonghuid_safe(yonghuid):
return club_id_for_tixian_yonghuid(yonghuid)
def filter_tixianjilu_by_request(qs, request):
return filter_queryset_by_club(qs, request, club_field='club_id')
def forbid_if_tixian_out_of_scope(request, tixian_record):
"""提现记录不在当前俱乐部范围时返回 Response。"""
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return None
club_id = resolve_club_id_from_request(request)
rec_club = getattr(tixian_record, 'club_id', None) or club_id_for_tixian_yonghuid_safe(
tixian_record.yonghuid,
)
if rec_club != club_id:
return Response({'code': 403, 'msg': '该提现记录不属于当前俱乐部数据范围'})
return None

View File

@@ -0,0 +1,80 @@
"""用户管理汇总统计(集团汇总 + 按俱乐部拆分)。"""
from django.utils import timezone
from jituan.constants import DATA_SCOPE_ALL
from jituan.models import Club
from jituan.services.club_context import (
filter_queryset_by_club,
resolve_club_id_from_request,
resolve_club_scope,
filter_user_qs_by_club,
filter_user_related_by_club,
)
from products.models import Huiyuangoumai
from users.business_models import User
from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang
def _dashou_valid_count(request, dashou_user_ids):
now = timezone.now()
hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request)
return hy_qs.filter(
yonghu_id__in=dashou_user_ids,
huiyuan_zhuangtai=1,
daoqi_time__gt=now,
).values('yonghu_id').distinct().count()
def _counts_for_club_id(club_id):
dashou_qs = UserDashou.query.filter(user__ClubID=club_id)
dashou_ids = dashou_qs.values_list('user__UserUID', flat=True)
now = timezone.now()
dashou_valid = Huiyuangoumai.query.filter(
club_id=club_id,
yonghu_id__in=dashou_ids,
huiyuan_zhuangtai=1,
daoqi_time__gt=now,
).values('yonghu_id').distinct().count()
shangjia_qs = UserShangjia.query.filter(user__ClubID=club_id)
return {
'club_id': club_id,
'dashou_total': dashou_qs.count(),
'dashou_valid': dashou_valid,
'guanshi_total': User.query.filter(ClubID=club_id, GuanshiProfile__isnull=False).count(),
'shangjia_total': shangjia_qs.count(),
'shangjia_valid': shangjia_qs.filter(zhuangtai=1).count(),
'zuzhang_total': UserZuzhang.query.filter(user__ClubID=club_id).count(),
'zuzhang_active': UserZuzhang.query.filter(user__ClubID=club_id, zhuangtai=1).count(),
}
def build_user_summary(request):
dashou_qs = filter_user_related_by_club(UserDashou.query.all(), request)
guanshi_qs = filter_user_qs_by_club(
User.query.filter(GuanshiProfile__isnull=False), request,
)
shangjia_qs = filter_user_related_by_club(UserShangjia.query.all(), request)
zuzhang_qs = filter_user_related_by_club(UserZuzhang.query.all(), request)
dashou_ids = dashou_qs.values_list('user__UserUID', flat=True)
result = {
'club_id': resolve_club_id_from_request(request),
'scope': resolve_club_scope(request),
'dashou_total': dashou_qs.count(),
'dashou_valid': _dashou_valid_count(request, dashou_ids),
'guanshi_total': guanshi_qs.count(),
'shangjia_total': shangjia_qs.count(),
'shangjia_valid': shangjia_qs.filter(zhuangtai=1).count(),
'zuzhang_total': zuzhang_qs.count(),
'zuzhang_active': zuzhang_qs.filter(zhuangtai=1).count(),
}
if resolve_club_scope(request) == DATA_SCOPE_ALL:
by_club = []
for club in Club.query.filter(status=1).order_by('sort_order', 'club_id'):
row = _counts_for_club_id(club.club_id)
row['name'] = club.name
by_club.append(row)
result['by_club'] = by_club
return result

View File

@@ -0,0 +1,209 @@
"""俱乐部微信登录(多 openid 绑定,不修改旧 wechatlogin 逻辑)。"""
import logging
import random
import time
import uuid
from django.db import transaction
from django.utils import timezone
from rest_framework_simplejwt.tokens import RefreshToken
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import UserWxOpenid
from jituan.services.club_resolver import jscode2session, resolve_club_by_appid
from users.business_models import User
from users.models import UserBoss, UserDashou, UserGuanshi, UserShangjia, UserZuzhang, UserShenheguan
from gvsdsdk.models import Role
logger = logging.getLogger(__name__)
def _generate_user_uid():
for _ in range(10):
timestamp_part = str(int(time.time()))[-5:].zfill(5)
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 User.query.filter(UserUID=user_id).exists():
return user_id
raise RuntimeError('生成用户ID失败')
def _ensure_boss_profile(user):
try:
UserBoss.query.get(user=user)
except UserBoss.DoesNotExist:
UserBoss.query.create(user=user, nickname='板板大人')
def _ensure_consumer_role(user):
from users.business_models import UserRole as BizUserRole
normal_role = Role.objects.filter(RoleName='消费者').first()
if not normal_role:
return
exists = BizUserRole.objects.filter(UserUUID=user.UserUUID, RoleUUID=normal_role.RoleUUID).exists()
if not exists:
BizUserRole.objects.create(
UserRoleUUID=uuid.uuid4().bytes,
UserUUID=user.UserUUID,
RoleUUID=normal_role.RoleUUID,
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
)
def _collect_role_status(user):
dashou_status = None
shangjia_status = None
guanshi_status = None
zuzhang_status = None
kaoheguan_status = 0
boss_nickname = '微信用户'
try:
boss = UserBoss.query.get(user=user)
boss_nickname = boss.nickname or '微信用户'
except UserBoss.DoesNotExist:
pass
try:
UserDashou.query.get(user=user)
dashou_status = 1
except UserDashou.DoesNotExist:
dashou_status = None
try:
UserShangjia.query.get(user=user)
shangjia_status = 1
except UserShangjia.DoesNotExist:
shangjia_status = None
try:
UserGuanshi.query.get(user=user)
guanshi_status = 1
except UserGuanshi.DoesNotExist:
guanshi_status = None
try:
UserZuzhang.query.get(user=user)
zuzhang_status = 1
except UserZuzhang.DoesNotExist:
zuzhang_status = None
try:
kg = UserShenheguan.query.get(user=user)
kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
except UserShenheguan.DoesNotExist:
kaoheguan_status = 0
return {
'dashou_status': dashou_status,
'shangjia_status': shangjia_status,
'guanshi_status': guanshi_status,
'zuzhang_status': zuzhang_status,
'kaoheguan_status': kaoheguan_status,
'boss_nickname': boss_nickname,
}
def login_or_register_by_club(code, club_id=None, app_id=None, client_ip=''):
"""
俱乐部微信登录核心逻辑。
- 通过 user_wx_openid(club_id, openid) 定位用户
- xq 俱乐部兼容:若绑定不存在但 User.OpenID 已存在,则自动建绑定(不新建用户)
"""
wx_data = jscode2session(code, club_id=club_id, app_id=app_id)
if not wx_data or 'openid' not in wx_data:
return None, wx_data.get('errmsg', '微信授权失败')
openid = wx_data['openid']
unionid = wx_data.get('unionid', '')
resolved_club_id = wx_data.get('club_id') or club_id or CLUB_ID_DEFAULT
with transaction.atomic():
binding = UserWxOpenid.query.filter(
club_id=resolved_club_id, openid=openid
).first()
user = None
created = False
if binding:
user = User.query.filter(UserUID=binding.yonghuid).first()
if not user:
user = User.objects.filter(UserUID=binding.yonghuid).first()
if not user and resolved_club_id == CLUB_ID_DEFAULT:
user = User.query.filter(OpenID=openid).first()
if not user:
user = User.objects.filter(OpenID=openid).first()
if user:
UserWxOpenid.query.create(
club_id=resolved_club_id,
openid=openid,
yonghuid=user.UserUID,
unionid=unionid or user.UnionID,
)
if not user:
user_uuid = uuid.uuid4().bytes
new_uid = _generate_user_uid()
user = User(
UserUUID=user_uuid,
UserUID=new_uid,
UserName=openid,
OpenID=openid if resolved_club_id == CLUB_ID_DEFAULT else None,
UnionID=unionid or None,
)
user.save()
created = True
_ensure_boss_profile(user)
_ensure_consumer_role(user)
UserWxOpenid.query.create(
club_id=resolved_club_id,
openid=openid,
yonghuid=user.UserUID,
unionid=unionid,
)
if unionid and unionid != (user.UnionID or ''):
user.UnionID = unionid
if client_ip:
user.IP = client_ip
user.UserLastLoginDate = timezone.now()
user.save()
from jituan.services.club_user import ensure_user_club_id
ensure_user_club_id(user, resolved_club_id)
roles = _collect_role_status(user)
refresh = RefreshToken.for_user(user)
token = str(refresh.access_token)
from merchant_ops.services.authz import staff_fields_for_login
from users.views import WechatMiniProgramLoginView
staff_fields = staff_fields_for_login(user)
login_view = WechatMiniProgramLoginView()
order_counts = login_view.jisuanOrderShuliang(user.UserUID)
group_info = login_view.huoquQunPeizhi()
data = {
'token': token,
'nicheng': roles['boss_nickname'],
'uid': user.UserUID,
'touxiang': user.Avatar or '',
'shangjiastatus': roles['shangjia_status'],
'dashoustatus': roles['dashou_status'],
'guanshistatus': roles['guanshi_status'],
'zuzhangstatus': roles['zuzhang_status'],
'kaoheguanstatus': roles['kaoheguan_status'],
'dingdantiaoshu': order_counts,
'dashouqun': group_info.get('dashouqun', ''),
'dashouqunid': group_info.get('dashouqunid', ''),
'guanshiqun': group_info.get('guanshiqun', ''),
'guanshiqunid': group_info.get('guanshiqunid', ''),
'club_id': resolved_club_id,
'is_new_user': created,
**staff_fields,
}
return data, None

View File

@@ -0,0 +1,160 @@
"""各俱乐部微信支付 V2MD5配置与验签。"""
import hashlib
import logging
from django.conf import settings
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
logger = logging.getLogger(__name__)
def get_wechat_v2_config(club_id=None):
"""
小程序 JSAPI 支付 V2 配置。
多 club 时读 club 表;缺省或 xq 回落 settings现网不变
"""
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
fallback = {
'appid': getattr(settings, 'WEIXIN_APPID', ''),
'mch_id': getattr(settings, 'WEIXIN_MCHID', ''),
'key': getattr(settings, 'WEIXIN_SHANGHUMIYAO', ''),
'club_id': CLUB_ID_DEFAULT,
}
if cid == CLUB_ID_DEFAULT:
return fallback
try:
club = Club.query.get(club_id=cid)
except Club.DoesNotExist:
logger.warning('club %s 不存在,回落全局支付配置', cid)
return fallback
cfg_json = club.config_json or {}
mch_key = (
cfg_json.get('mch_key')
or cfg_json.get('shanghumiyao')
or cfg_json.get('WEIXIN_SHANGHUMIYAO')
or fallback['key']
)
return {
'appid': club.pay_app_id or club.wx_appid or fallback['appid'],
'mch_id': club.mch_id or fallback['mch_id'],
'key': mch_key,
'club_id': cid,
}
def verify_wechat_v2_signature(xml_data, club_id=None):
"""验证微信 V2 支付回调 MD5 签名。"""
try:
import defusedxml.ElementTree as ET
root = ET.fromstring(xml_data)
sign_node = root.find('sign')
if sign_node is None or not sign_node.text:
return False
sign = sign_node.text
data = {child.tag: child.text for child in root if child.tag != 'sign'}
string_a = '&'.join([f'{k}={data[k]}' for k in sorted(data.keys()) if data[k]])
pay_cfg = get_wechat_v2_config(club_id)
calc = hashlib.md5(f'{string_a}&key={pay_cfg["key"]}'.encode('utf-8')).hexdigest().upper()
if calc == sign:
return True
# 兼容:子 club 未配密钥时尝试全局密钥
global_key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
if global_key and global_key != pay_cfg['key']:
calc2 = hashlib.md5(f'{string_a}&key={global_key}'.encode('utf-8')).hexdigest().upper()
return calc2 == sign
return False
except Exception as e:
logger.error('微信验签异常: %s', e)
return False
def club_id_from_czjilu(dingdan_id):
from products.models import Czjilu
try:
row = Czjilu.query.filter(dingdan_id=dingdan_id).first()
if row and getattr(row, 'club_id', None):
return row.club_id
except Exception:
pass
return CLUB_ID_DEFAULT
def club_id_from_order(order_id):
from orders.models import Order
try:
row = Order.query.filter(OrderID=order_id).first()
if row and getattr(row, 'ClubID', None):
return row.ClubID
except Exception:
pass
return CLUB_ID_DEFAULT
def get_wechat_v3_config(club_id=None):
"""
微信 V3转账/查单)配置。
多 club 读 club 表;缺省或 xq 回落 settings.WECHAT_PAY_V3_CONFIG。
"""
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
wx = dict(getattr(settings, 'WECHAT_PAY_V3_CONFIG', {}) or {})
fallback = {
'APPID': wx.get('APPID', ''),
'MCHID': wx.get('MCHID', ''),
'PRIVATE_KEY_PATH': wx.get('PRIVATE_KEY_PATH', ''),
'CERT_SERIAL_NO': wx.get('CERT_SERIAL_NO', ''),
'API_V3_KEY': wx.get('API_V3_KEY', ''),
'PLATFORM_CERT_DIR': wx.get('PLATFORM_CERT_DIR', ''),
'TRANSFER_SCENE_ID': wx.get('TRANSFER_SCENE_ID', '1005'),
'club_id': CLUB_ID_DEFAULT,
}
if cid == CLUB_ID_DEFAULT:
return fallback
try:
club = Club.query.get(club_id=cid)
except Club.DoesNotExist:
logger.warning('club %s 不存在,回落全局 V3 支付配置', cid)
return fallback
cfg_json = club.config_json or {}
return {
'APPID': club.pay_app_id or club.wx_appid or fallback['APPID'],
'MCHID': club.mch_id or fallback['MCHID'],
'PRIVATE_KEY_PATH': (
club.private_key_path
or cfg_json.get('private_key_path')
or fallback['PRIVATE_KEY_PATH']
),
'CERT_SERIAL_NO': (
club.cert_serial_no
or cfg_json.get('cert_serial_no')
or fallback['CERT_SERIAL_NO']
),
'API_V3_KEY': (
club.api_v3_key
or cfg_json.get('api_v3_key')
or fallback['API_V3_KEY']
),
'PLATFORM_CERT_DIR': (
club.platform_cert_dir
or cfg_json.get('platform_cert_dir')
or fallback['PLATFORM_CERT_DIR']
),
'TRANSFER_SCENE_ID': (
cfg_json.get('transfer_scene_id')
or cfg_json.get('TRANSFER_SCENE_ID')
or fallback['TRANSFER_SCENE_ID']
),
'club_id': cid,
}
def club_id_for_tixian_yonghuid(yonghuid):
from jituan.services.club_user import get_user_club_id
from users.business_models import User
user = User.query.filter(UserUID=yonghuid).first()
return get_user_club_id(user)

View File

@@ -0,0 +1,267 @@
"""提现设置与运行时:按俱乐部读 club_lilubiao / club_tixian_quota / club_withdraw_config。"""
from datetime import date
from decimal import Decimal
from django.db import transaction
from rest_framework.response import Response
from backend.models import WithdrawalDailyStats
from config.models import TixianQuotaDefault
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.models import ClubLilubiao, ClubTixianQuota, ClubWithdrawConfig
from jituan.services.club_config import get_commission_rate
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
# 提现手续费率 → lilubiao config_type / CommissionRate Platform
LEIXING_RATE_KEY = {
1: '5', # 打手佣金
2: '6', # 管事分红
3: '8', # 组长分红
4: '9', # 考核官
5: '11', # 打手押金
6: '10', # 商家余额
}
# 提现身份 leixing → 平台日总限额配置 leixingclub_tixian_quota / TixianQuotaDefault UserType
WITHDRAW_TOTAL_QUOTA_LEIXING = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
def _normalize_club_id(club_id):
return (club_id or '').strip() or CLUB_ID_DEFAULT
def _club_id(request):
return resolve_club_id_from_request(request)
def _get_quota(club_id, leixing, default=20.0):
club_id = _normalize_club_id(club_id)
row = ClubTixianQuota.query.filter(club_id=club_id, leixing=leixing).first()
if row is not None:
return float(row.daily_limit)
obj = TixianQuotaDefault.query.filter(UserType=leixing).first()
return float(obj.default_quota) if obj else default
def get_quota_decimal(club_id, leixing, default=20.0):
return Decimal(str(_get_quota(club_id, leixing, default)))
def _set_quota(club_id, leixing, quota):
row, created = ClubTixianQuota.query.get_or_create(
club_id=_normalize_club_id(club_id),
leixing=leixing,
defaults={'daily_limit': quota},
)
if not created:
row.daily_limit = quota
row.save(update_fields=['daily_limit', 'UpdateTime'])
def _set_lilv(club_id, config_type, rate):
ct = int(config_type)
row, created = ClubLilubiao.query.get_or_create(
club_id=_normalize_club_id(club_id),
config_type=ct,
defaults={'lilv': rate, 'remark': ''},
)
if not created:
row.lilv = rate
row.save(update_fields=['lilv', 'UpdateTime'])
def get_tixian_feilv(club_id, leixing):
"""读取提现手续费率(各俱乐部可不同)。"""
rate_key = LEIXING_RATE_KEY.get(leixing)
if not rate_key:
raise ValueError('无效的提现类型')
return get_commission_rate(_normalize_club_id(club_id), rate_key)
def get_withdraw_mode(club_id):
club_id = _normalize_club_id(club_id)
try:
return ClubWithdrawConfig.query.get(club_id=club_id).mode
except ClubWithdrawConfig.DoesNotExist:
return 2
def get_personal_daily_quota(club_id, leixing, profile):
"""打手/管事/组长个人每日限额:用户额外限额优先,否则读俱乐部配置。"""
if leixing == 1:
if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0:
return Decimal(str(profile.ewai_xiane))
return get_quota_decimal(club_id, 1)
if leixing == 2:
if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0:
return Decimal(str(profile.ewai_xiane))
return get_quota_decimal(club_id, 2)
if leixing == 3:
default = get_quota_decimal(club_id, 3, default=0.0)
if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0:
return Decimal(str(profile.ewai_tixian_xiane))
return default
raise ValueError('无效的个人限额身份')
def get_platform_daily_limit(club_id, leixing):
quota_leixing = WITHDRAW_TOTAL_QUOTA_LEIXING.get(leixing)
if not quota_leixing:
return Decimal('0.00')
return get_quota_decimal(club_id, quota_leixing, default=0.0)
def get_platform_daily_total(club_id, leixing, today=None):
"""平台今日已提现总额(到账金额口径,按俱乐部)。"""
club_id = _normalize_club_id(club_id)
today = today or date.today()
rec = WithdrawalDailyStats.query.filter(
club_id=club_id, Date=today, WithdrawalType=leixing,
).first()
if rec:
return Decimal(str(rec.total_amount))
return Decimal('0.00')
def get_or_create_platform_daily_stat(club_id, leixing, today=None):
"""在 transaction.atomic 内获取并锁定平台日统计行。"""
club_id = _normalize_club_id(club_id)
today = today or date.today()
stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create(
club_id=club_id,
Date=today,
WithdrawalType=leixing,
defaults={'total_amount': Decimal('0.00')},
)
return stat
def build_tixian_asset_rates(club_id):
club_id = _normalize_club_id(club_id)
return {
'dashou_rate': float(get_commission_rate(club_id, '5')),
'dashou_yajin_rate': float(get_commission_rate(club_id, '11')),
'shangjia_rate': float(get_commission_rate(club_id, '10')),
'guanshi_rate': float(get_commission_rate(club_id, '6')),
'zuzhang_rate': float(get_commission_rate(club_id, '8')),
'kaoheguan_rate': float(get_commission_rate(club_id, '9')),
}
def build_withdraw_settings_payload(request):
club_id = _club_id(request)
rate_map = {}
for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
rate_map[leixing] = float(get_commission_rate(club_id, code))
order_rate_map = {}
for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]:
order_rate_map[leixing] = float(get_commission_rate(club_id, code))
quota_map = {leixing: _get_quota(club_id, leixing) for leixing in [1, 2, 3]}
total_limit_map = {}
for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]:
total_limit_map[withdraw_leixing] = _get_quota(club_id, quota_leixing, default=0.0)
today = date.today()
today_totals = {i: 0.0 for i in range(1, 7)}
for rec in WithdrawalDailyStats.query.filter(club_id=club_id, Date=today):
if rec.WithdrawalType in today_totals:
today_totals[rec.WithdrawalType] = float(rec.total_amount)
return {
'club_id': club_id,
'scope': resolve_club_scope(request),
'withdraw_mode': get_withdraw_mode(club_id),
'rates': rate_map,
'order_rates': order_rate_map,
'quotas': quota_map,
'total_limits': total_limit_map,
'today_totals': today_totals,
}
def apply_withdraw_settings_update(request, permissions):
if resolve_club_scope(request) == DATA_SCOPE_ALL:
return Response({'code': 403, 'msg': '请在子公司视图下修改提现设置'})
club_id = _club_id(request)
rates = request.data.get('rates', {})
order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
quotas = request.data.get('quotas', {})
total_limits = request.data.get('total_limits', {})
role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'}
total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
def _rate_key_present(data, role):
return (str(role) in data and data[str(role)] is not None) or \
(role in data and data[role] is not None)
with transaction.atomic():
for role in [1, 2, 3]:
perm_needed = role_perms[role]
if perm_needed not in permissions:
if _rate_key_present(rates, role) or \
(str(role) in quotas and quotas[str(role)] is not None) or \
(role in quotas and quotas.get(role) is not None) or \
(str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed}'})
extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
for role in [4, 5, 6]:
if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
if (str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
for role in [1, 3, 12, 13]:
if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
order_names = {
1: '平台订单打手分红', 3: '商家订单打手分红',
12: '押金管事分红', 13: '管事订单分红',
}
return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
for role in [1, 2, 3, 4, 5, 6]:
val = rates.get(str(role), rates.get(role))
if val is not None:
_set_lilv(club_id, rate_code_map[role], Decimal(str(val)))
for role in [1, 3, 12, 13]:
val = order_rates.get(str(role), order_rates.get(role))
if val is not None:
_set_lilv(club_id, order_rate_code_map[role], Decimal(str(val)))
for role in [1, 2, 3]:
if str(role) in quotas:
val = quotas[str(role)]
if val is not None:
_set_quota(club_id, role, Decimal(str(val)))
elif role in quotas and quotas.get(role) is not None:
_set_quota(club_id, role, Decimal(str(quotas[role])))
for role in [1, 2, 3, 4, 5, 6]:
val = total_limits.get(str(role), total_limits.get(role))
if val is not None:
_set_quota(club_id, total_code_map[role], Decimal(str(val)))
withdraw_mode = request.data.get('withdraw_mode')
if withdraw_mode is not None:
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限修改提现模式'})
row, created = ClubWithdrawConfig.query.get_or_create(
club_id=club_id,
defaults={'mode': int(withdraw_mode)},
)
if not created:
row.mode = int(withdraw_mode)
row.save(update_fields=['mode', 'UpdateTime'])
return Response({'code': 0, 'msg': '设置修改成功'})

View File

@@ -0,0 +1,130 @@
-- 集团多俱乐部:业务表 club_id 扩展(默认 xq现网数据自动归属星阙
-- 执行前请备份数据库。可重复执行(列已存在则跳过)。
-- User 表俱乐部归属
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'User' AND COLUMN_NAME = 'ClubID'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `User` ADD COLUMN `ClubID` varchar(16) NOT NULL DEFAULT ''xq'' COMMENT ''所属俱乐部''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 订单
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'dingdan' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `dingdan` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq'', ADD INDEX `idx_dingdan_club` (`club_id`)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 充值流水
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'czjilu' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `czjilu` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq'', ADD INDEX `idx_czjilu_club` (`club_id`)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 会员购买记录
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'huiyuangoumai' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `huiyuangoumai` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 提现记录
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tixianjilu' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `tixianjilu` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 罚单
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fadan' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `fadan` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 平台收支汇总
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'szjilu' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `szjilu` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 轮播club_id + page_key
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lunbo' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `lunbo` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'lunbo' AND COLUMN_NAME = 'page_key'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `lunbo` ADD COLUMN `page_key` varchar(32) NOT NULL DEFAULT ''order_pool''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 弹窗页
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'popup_page' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `popup_page` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 商品 catalog
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'shangpin' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `shangpin` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq''',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 每日收入/出款统计(按俱乐部)
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'daily_income_stat' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `daily_income_stat` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq'', ADD INDEX `idx_income_club_date` (`club_id`, `date`)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @exist := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'daily_payout_stat' AND COLUMN_NAME = 'club_id'
);
SET @sql := IF(@exist = 0,
'ALTER TABLE `daily_payout_stat` ADD COLUMN `club_id` varchar(16) NOT NULL DEFAULT ''xq'', ADD INDEX `idx_payout_club_date` (`club_id`, `date`)',
'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;

70
jituan/urls.py Normal file
View File

@@ -0,0 +1,70 @@
from django.urls import path
from backend.view import (
FaKuanChuLiView,
FaKuanChuangJianView,
FaKuanLieBiaoView,
FaKuanTongJiView,
GetGuanliListView,
GetOperationLogListView,
GetWithdrawSettingsView,
GetZuzhangListView,
KefuGetDashouListView,
KefuGetShangjiaListView,
PopupNoticeListAPIView,
PopupNoticeModifyAPIView,
UpdateWithdrawSettingsView,
)
from users.views import KefuPunishmentListView
from jituan.views_display import ClubGonggaoView, ClubLunboListView, ClubLunboManageView
from jituan.views import (
ClubAuditLogListView,
ClubCaiwuView,
ClubChongzhiFinanceView,
ClubDashouRegisterView,
ClubHuiyuanBankuaiView,
ClubHuiyuanStatsView,
ClubKefuLoginView,
ClubListView,
ClubMeContextView,
ClubOrderFinanceView,
ClubOrderTypesView,
ClubSzxxView,
ClubUserSummaryView,
ClubWechatLoginView,
)
urlpatterns = [
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'),
path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'),
path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'),
path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'),
path('houtai/szxx', ClubSzxxView.as_view(), name='jituan_szxx'),
path('houtai/audit-log', ClubAuditLogListView.as_view(), name='jituan_audit_log'),
path('houtai/hqczrz', GetOperationLogListView.as_view(), name='jituan_operation_log'),
path('houtai/hthqtxpz', GetWithdrawSettingsView.as_view(), name='jituan_withdraw_get'),
path('houtai/htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='jituan_withdraw_update'),
path('houtai/cwddhqlx', ClubOrderTypesView.as_view(), name='jituan_cwddhqlx'),
path('houtai/cwhqjtddsj', ClubOrderFinanceView.as_view(), name='jituan_cwhqjtddsj'),
path('houtai/cwqtczhq', ClubChongzhiFinanceView.as_view(), name='jituan_cwqtczhq'),
path('houtai/cwhybkhq', ClubHuiyuanBankuaiView.as_view(), name='jituan_cwhybkhq'),
path('houtai/hybkjtsj', ClubHuiyuanStatsView.as_view(), name='jituan_hybkjtsj'),
path('houtai/user-summary', ClubUserSummaryView.as_view(), name='jituan_user_summary'),
# 用户列表(与 /houtai/* 同一视图,按 X-Club-Id 过滤;详情仍走 /houtai/*
path('houtai/kefuhqdslb', KefuGetDashouListView.as_view(), name='jituan_dashou_list'),
path('houtai/hthqgslb', GetGuanliListView.as_view(), name='jituan_guanshi_list'),
path('houtai/hqsjgllb', KefuGetShangjiaListView.as_view(), name='jituan_shangjia_list'),
path('houtai/hthqzzlb', GetZuzhangListView.as_view(), name='jituan_zuzhang_list'),
path('houtai/lunbo-list', ClubLunboListView.as_view(), name='jituan_lunbo_list'),
path('houtai/lunbo-manage', ClubLunboManageView.as_view(), name='jituan_lunbo_manage'),
path('houtai/gonggao', ClubGonggaoView.as_view(), name='jituan_gonggao'),
path('houtai/hthqtcxx', PopupNoticeListAPIView.as_view(), name='jituan_popup_list'),
path('houtai/htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='jituan_popup_modify'),
path('houtai/htglyhqcfsltj', FaKuanTongJiView.as_view(), name='jituan_penalty_stats'),
path('houtai/hthqfklb', FaKuanLieBiaoView.as_view(), name='jituan_penalty_list'),
path('houtai/glyclfk', FaKuanChuLiView.as_view(), name='jituan_penalty_action'),
path('houtai/htfksc', FaKuanChuangJianView.as_view(), name='jituan_penalty_create'),
path('houtai/kefu-cfgl', KefuPunishmentListView.as_view(), name='jituan_kefu_cfgl'),
path('club/list', ClubListView.as_view(), name='jituan_club_list'),
]

477
jituan/views.py Normal file
View File

@@ -0,0 +1,477 @@
"""集团平台 API 视图。"""
import logging
from django.utils import timezone
from rest_framework import status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from jituan.models import Club
from jituan.services.admin_context import build_admin_club_context
from jituan.services.wechat_login import login_or_register_by_club
from jituan.services.caiwu_stats import build_caiwu_payload
from jituan.services.szxx_stats import build_szxx_payload
from jituan.services.admin_audit import list_admin_audit_logs
from jituan.services.user_stats import build_user_summary
from jituan.services.finance_detail_stats import (
build_chongzhi_finance_payload,
build_huiyuan_bankuai_payload,
build_huiyuan_stats_payload,
build_order_finance_payload,
build_order_types_payload,
)
from backend.utils import verify_kefu_permission
from users.business_models import User
logger = logging.getLogger(__name__)
def _client_ip(request):
xff = request.META.get('HTTP_X_FORWARDED_FOR')
if xff:
return xff.split(',')[0].strip()
return request.META.get('REMOTE_ADDR', '')
class ClubWechatLoginView(APIView):
"""
多俱乐部微信小程序登录(新接口,旧 /yonghu/wechatlogin 不变)。
POST /jituan/auth/wechat-login
body: { code, club_id?, app_id? }
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
code = (request.data.get('code') or '').strip()
club_id = (request.data.get('club_id') or '').strip() or None
app_id = (request.data.get('app_id') or '').strip() or None
if not code:
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
try:
data, err = login_or_register_by_club(
code, club_id=club_id, app_id=app_id, client_ip=_client_ip(request),
)
if err:
return Response({'code': 2, 'msg': f'微信登录失败: {err}', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
return Response({'code': 0, 'msg': '登录成功', 'data': data})
except Exception as e:
logger.exception('ClubWechatLoginView 异常')
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ClubKefuLoginView(APIView):
"""
多俱乐部客服/集团后台登录(新接口,旧 /yonghu/kefujinru 不变)。
POST /jituan/auth/kefu-login
body: { phone, password, erjimima }
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
phone = (request.data.get('phone') or '').strip()
password = request.data.get('password') or ''
erjimima = request.data.get('erjimima') or ''
if not phone or not password:
return Response({'code': 1, 'msg': '请填写手机号和密码', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
user_mains = list(
User.query.filter(Phone=phone, KefuProfile__isnull=False).select_related('KefuProfile')
)
if not user_mains:
admin_candidates = User.query.filter(Phone=phone)
for admin_user in admin_candidates:
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
user_mains = [admin_user]
break
if not user_mains:
return Response({'code': 1, 'msg': '用户不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
target_user = None
target_kefu = None
password_matched = False
for user in user_mains:
if not user.CheckPassword(password):
continue
password_matched = True
is_admin = user.IsSuperuser or user.UserType == 'admin'
if is_admin:
target_user = user
target_kefu = None
break
kefu = user.KefuProfile
stored_erji = kefu.erjimima or ''
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
import bcrypt as _bcrypt
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
continue
elif stored_erji != erjimima:
continue
if kefu.zhuangtai != 1:
if target_user is None:
target_user = user
target_kefu = kefu
continue
target_user = user
target_kefu = kefu
break
else:
if target_user and target_kefu:
return Response({'code': 4, 'msg': '账号已被封禁', 'data': None},
status=status.HTTP_403_FORBIDDEN)
if password_matched:
return Response({'code': 1, 'msg': '二级密码错误', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
return Response({'code': 1, 'msg': '密码错误', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
target_user.IP = _client_ip(request)
target_user.UserLastLoginDate = timezone.now()
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
club_context = build_admin_club_context(target_user)
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
'club_context': club_context,
},
})
class ClubListView(APIView):
"""GET /jituan/club/list — 启用中的俱乐部列表(登录前后均可读)。"""
permission_classes = [AllowAny]
def get(self, request):
clubs = Club.query.filter(status=1).order_by('sort_order', 'club_id')
data = [
{
'club_id': c.club_id,
'name': c.name,
'wx_appid': c.wx_appid,
'h5_domain': c.h5_domain,
}
for c in clubs
]
return Response({'code': 0, 'msg': 'ok', 'data': data})
class ClubMeContextView(APIView):
"""GET /jituan/auth/me-context — 已登录客服刷新俱乐部上下文。"""
permission_classes = [IsAuthenticated]
def get(self, request):
user = request.user
if user.UserType not in ('kefu', 'admin') and not user.IsSuperuser:
return Response({'code': 403, 'msg': '非后台账号', 'data': None}, status=403)
return Response({
'code': 0,
'msg': 'ok',
'data': build_admin_club_context(user),
})
class ClubCaiwuView(APIView):
"""
俱乐部维度财务(新接口)。
旧 POST /houtai/caiwu 完全不变kefu 切换俱乐部后应调本接口。
Header: X-Club-Id, 集团汇总 X-Club-Scope: all
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
try:
data = build_caiwu_payload(request)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubCaiwuView 异常')
return Response({'code': 99, 'msg': '统计失败', 'data': None}, status=500)
class ClubSzxxView(APIView):
"""
俱乐部维度每日收支详细统计(新接口)。
旧 POST /houtai/szxx 不变;参数与旧接口一致。
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'caiwu' not in permissions:
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
granularity = request.data.get('granularity', 'day')
year = request.data.get('year')
month = request.data.get('month')
summary_date = request.data.get('summary_date')
try:
if year is not None:
year = int(year)
if month is not None:
month = int(month)
except (ValueError, TypeError):
return Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
try:
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubSzxxView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubAuditLogListView(APIView):
"""
后台审计日志admin_audit_log
POST /jituan/houtai/audit-log
权限000001 或 czrz666与操作日志一致
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = (request.data.get('username') or request.data.get('phone') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions and 'czrz666' not in permissions:
return Response({'code': 403, 'msg': '无权限查看审计日志'})
try:
page = int(request.data.get('page', 1))
except (ValueError, TypeError):
page = 1
try:
page_size = int(request.data.get('page_size', 20))
except (ValueError, TypeError):
page_size = 20
data = list_admin_audit_logs(
request,
page=page,
page_size=page_size,
operator_yonghuid=request.data.get('operator_yonghuid'),
target_yonghuid=request.data.get('target_yonghuid'),
action=request.data.get('action'),
keyword=request.data.get('keyword'),
)
return Response({'code': 0, 'msg': 'ok', 'data': data})
def _parse_year_month(data):
year = data.get('year')
month = data.get('month')
try:
if year is not None:
year = int(year)
if month is not None:
month = int(month)
except (ValueError, TypeError):
return None, None, Response({'code': 1, 'msg': '年份或月份格式错误'}, status=400)
return year, month, None
class ClubOrderTypesView(APIView):
"""POST /jituan/houtai/cwddhqlx — 订单财务商品类型筛选。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
return Response({'code': 0, 'msg': 'ok', 'data': build_order_types_payload()})
class ClubOrderFinanceView(APIView):
"""POST /jituan/houtai/cwhqjtddsj — 订单财务详细统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'dingdancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_order_finance_payload(
request,
request.data.get('granularity', 'day'),
year,
month,
request.data.get('type_id', 0),
request.data.get('order_source', 0),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubOrderFinanceView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubChongzhiFinanceView(APIView):
"""POST /jituan/houtai/cwqtczhq — 其他充值/罚款财务统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'qtczcaiwu' not in permissions:
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_chongzhi_finance_payload(
request,
request.data.get('granularity', 'day'),
year,
month,
request.data.get('types', [2, 3, 4, 5, 6]),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubChongzhiFinanceView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubHuiyuanBankuaiView(APIView):
"""POST /jituan/houtai/cwhybkhq — 板块及会员列表(附带俱乐部售价)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
payload = build_huiyuan_bankuai_payload(request)
return Response({
'code': 0,
'msg': 'ok',
'data': payload['bankuai_list'],
'club_id': payload['club_id'],
'scope': payload['scope'],
})
class ClubHuiyuanStatsView(APIView):
"""POST /jituan/houtai/hybkjtsj — 会员财务详细统计(按俱乐部)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('phone', None)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
year, month, err_resp = _parse_year_month(request.data)
if err_resp:
return err_resp
try:
data, err = build_huiyuan_stats_payload(
request,
request.data.get('huiyuan_id'),
request.data.get('granularity', 'day'),
year,
month,
request.data.get('include_guanshi', False),
request.data.get('include_zuzhang', False),
request.data.get('summary_date'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubHuiyuanStatsView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubUserSummaryView(APIView):
"""
用户管理汇总(打手/管事/商家/组长数量)。
集团视图 X-Club-Scope: all 时额外返回 by_club 各公司拆分。
POST /jituan/houtai/user-summary
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = (request.data.get('phone') or request.data.get('username') or '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少账号'}, status=401)
kefu_obj, permissions = verify_kefu_permission(request, username)
if kefu_obj is None:
return permissions
try:
data = build_user_summary(request)
return Response({'code': 0, 'msg': 'ok', 'data': data})
except Exception:
logger.exception('ClubUserSummaryView 异常')
return Response({'code': 99, 'msg': '统计失败'}, status=500)
class ClubDashouRegisterView(APIView):
"""
打手邀请码注册(带俱乐部校验)。
POST /jituan/auth/dashou-register body: { inviteCode }
与 /yonghu/dashouzhuce 逻辑一致,推荐新小程序使用。
"""
permission_classes = [IsAuthenticated]
def post(self, request):
from users.views import DashouZhuceView
return DashouZhuceView().post(request)

157
jituan/views_display.py Normal file
View File

@@ -0,0 +1,157 @@
"""小程序展示配置:轮播图 / 公告(按俱乐部)。"""
import os
import random
import string
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from backend.utils import verify_kefu_permission
from config.models import Gonggao, Lunbo
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.display_config import (
forbid_display_write_in_all_scope,
get_gonggao_content,
get_lunbo_urls,
list_response_meta,
LUNBO_PAGE_DASHOU_CENTER,
LUNBO_PAGE_OPTIONS,
normalize_page_key,
)
from products.views import delete_from_oss, upload_to_oss, validate_image
POPUP_PERM = '8080a'
class ClubLunboListView(APIView):
"""GET 轮播列表 POST /jituan/houtai/lunbo-list"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
if not phone:
return Response({'code': 401, 'msg': '缺少客服账号'})
kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None:
return permissions
if POPUP_PERM not in permissions:
return Response({'code': 403, 'msg': '无权限管理轮播图'})
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
image_type = 2 if page_key == LUNBO_PAGE_DASHOU_CENTER else 1
urls = get_lunbo_urls(request, page_key=page_key, image_type=image_type)
return Response({
'code': 0,
'data': {
'page_key': page_key,
'list': urls,
'page_options': [{'key': k, 'label': v} for k, v in LUNBO_PAGE_OPTIONS],
**list_response_meta(request),
},
})
class ClubLunboManageView(APIView):
"""轮播增删 POST /jituan/houtai/lunbo-manage"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
if not phone:
return Response({'code': 401, 'msg': '缺少客服账号'})
kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None:
return permissions
if POPUP_PERM not in permissions:
return Response({'code': 403, 'msg': '无权限管理轮播图'})
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
action = (request.data.get('action') or '').strip()
club_id = resolve_club_id_from_request(request)
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
image_type = 2 if page_key == LUNBO_PAGE_DASHOU_CENTER else 1
if action == 'delete':
tupian_url = (request.data.get('tupian_url') or '').strip()
if not tupian_url:
return Response({'code': 400, 'msg': '图片URL不能为空'})
row = Lunbo.query.filter(
club_id=club_id, ImageURL=tupian_url, ImageType=image_type, page_key=page_key,
).first()
if not row:
return Response({'code': 404, 'msg': '轮播图不存在'})
delete_from_oss(tupian_url)
row.delete()
return Response({'code': 0, 'msg': '删除成功'})
if action == 'add':
file_obj = request.FILES.get('file')
if not file_obj:
return Response({'code': 400, 'msg': '请上传图片'})
ok, msg = validate_image(file_obj)
if not ok:
return Response({'code': 400, 'msg': msg})
ext = os.path.splitext(file_obj.name)[1].lower() or '.jpg'
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
oss_file_path = f"a_long/lunbo/{random_str}{ext}"
if not upload_to_oss(file_obj, oss_file_path):
return Response({'code': 500, 'msg': '图片上传失败'})
Lunbo.query.create(
club_id=club_id,
page_key=page_key,
ImageURL=oss_file_path,
ImageType=image_type,
)
return Response({'code': 0, 'msg': '添加成功', 'data': {'tupian_url': oss_file_path}})
return Response({'code': 400, 'msg': 'action 必须为 add 或 delete'})
class ClubGonggaoView(APIView):
"""公告读写 POST /jituan/houtai/gonggao"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
if not phone:
return Response({'code': 401, 'msg': '缺少客服账号'})
kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None:
return permissions
if POPUP_PERM not in permissions:
return Response({'code': 403, 'msg': '无权限管理公告'})
notice_type = int(request.data.get('notice_type', 1) or 1)
club_id = resolve_club_id_from_request(request)
content = request.data.get('content')
if content is None:
return Response({
'code': 0,
'data': {
'content': get_gonggao_content(request, notice_type=notice_type),
'notice_type': notice_type,
**list_response_meta(request),
},
})
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
row, _ = Gonggao.query.get_or_create(
club_id=club_id,
NoticeType=notice_type,
defaults={'Content': content},
)
row.Content = content
row.save(update_fields=['Content', 'UpdateTime'])
return Response({'code': 0, 'msg': '保存成功'})

View File

@@ -0,0 +1,24 @@
# Generated manually for jituan multi-club
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0003_delete_dingdan_delete_dingdanpingtai_and_more'),
]
operations = [
migrations.AddField(
model_name='order',
name='ClubID',
field=models.CharField(
db_column='club_id',
db_index=True,
default='xq',
max_length=16,
verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0004_order_clubid'),
]
operations = [
migrations.AddField(
model_name='penalty',
name='ClubID',
field=models.CharField(
db_column='club_id',
db_index=True,
default='xq',
max_length=16,
verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0005_penalty_clubid'),
]
operations = [
migrations.AddField(
model_name='penaltybonus',
name='ClubID',
field=models.CharField(
db_column='club_id',
db_index=True,
default='xq',
max_length=16,
verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,22 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0006_penaltybonus_clubid'),
]
operations = [
migrations.AddField(
model_name='penaltyrecord',
name='ClubID',
field=models.CharField(
db_column='club_id',
db_index=True,
default='xq',
max_length=16,
verbose_name='所属俱乐部',
),
),
]

View File

@@ -51,6 +51,10 @@ class Order(QModel):
null=True, blank=True,
db_column='fadan_pingtai', verbose_name='发单平台',
)
ClubID = models.CharField(
max_length=16, default='xq', db_index=True,
db_column='club_id', verbose_name='所属俱乐部',
)
# ---- 金额 ----
Amount = models.DecimalField(
max_digits=10, decimal_places=2, null=True, blank=True,
@@ -502,6 +506,10 @@ class Penalty(QModel):
max_digits=12, decimal_places=2, default=0.00,
db_column='shenqingren_fenhong_jine', verbose_name='申请人分红金额',
)
ClubID = models.CharField(
max_length=16, default='xq', db_index=True,
db_column='club_id', verbose_name='所属俱乐部',
)
class Meta:
db_table = 'fadan'
@@ -599,6 +607,10 @@ class PenaltyBonus(QModel):
auto_now=True,
verbose_name='更新时间',
)
ClubID = models.CharField(
max_length=16, default='xq', db_index=True,
db_column='club_id', verbose_name='所属俱乐部',
)
class Meta:
db_table = 'fadan_fenhong'
@@ -762,6 +774,10 @@ class PenaltyRecord(QModel):
max_length=32, null=True, db_index=True,
db_column='dingdan_id', verbose_name='订单ID',
)
ClubID = models.CharField(
max_length=16, default='xq', db_index=True,
db_column='club_id', verbose_name='所属俱乐部',
)
CreateTime = models.DateTimeField(
auto_now_add=True,
verbose_name='创建时间',

View File

@@ -7,7 +7,9 @@ from django.db.models import F
from config.models import DailyIncomeStat, DailyPayoutStat
from orders.models import Order, PlatformOrderExt, MerchantOrderExt, CommissionRate
from jituan.services.club_config import get_commission_rate
from products.models import Gsfenhong
from jituan.services.club_penalty import resolve_gsfenhong_club_id
from users.models import UserDashou, UserGuanshi
from backend.utils import update_guanshi_daily_by_action
@@ -31,71 +33,66 @@ def get_order_user_id(order):
return None
def update_daily_income(amount):
def update_daily_income(amount, club_id=None):
"""
原子更新当日收入统计金额累加笔数加1
用于微信支付成功回调。
参数:
amount: Decimal 本次收入金额
"""
from jituan.constants import CLUB_ID_DEFAULT
cid = club_id or CLUB_ID_DEFAULT
today = date.today()
year, month, day = today.year, today.month, today.day
with transaction.atomic():
stat, created = DailyIncomeStat.objects.select_for_update().get_or_create(
date=today,
club_id=cid,
defaults={
'year': year,
'month': month,
'day': day,
'total_amount': amount,
'total_count': 1
}
'total_count': 1,
},
)
if not created:
# 原子累加
stat.total_amount = F('total_amount') + amount
stat.total_count = F('total_count') + 1
stat.save(update_fields=['total_amount', 'total_count'])
logger.info(
f"每日收入更新: {today}, +{amount}, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}")
f"每日收入更新[{cid}]: {today}, +{amount}"
)
def update_daily_payout(amount):
def update_daily_payout(amount, club_id=None):
"""
原子更新当日出款统计金额累加笔数加1
用于提现成功、结算打款等场景。
参数:
amount: Decimal 本次出款金额
"""
from jituan.constants import CLUB_ID_DEFAULT
cid = club_id or CLUB_ID_DEFAULT
today = date.today()
year, month, day = today.year, today.month, today.day
with transaction.atomic():
stat, created = DailyPayoutStat.objects.select_for_update().get_or_create(
date=today,
club_id=cid,
defaults={
'year': year,
'month': month,
'day': day,
'total_amount': amount,
'total_count': 1
}
'total_count': 1,
},
)
if not created:
# 原子累加
stat.total_amount = F('total_amount') + amount
stat.total_count = F('total_count') + 1
stat.save(update_fields=['total_amount', 'total_count'])
logger.info(f"每日出款更新: {today}, +{amount}元, 总金额={stat.total_amount if created else stat.total_amount + amount}, 总笔数={stat.total_count if created else stat.total_count + 1}")
logger.info(f"每日出款更新[{cid}]: {today}, +{amount}")
@@ -104,24 +101,19 @@ def update_daily_payout(amount):
def calc_shangjia_order_fencheng(jine):
def calc_shangjia_order_fencheng(jine, club_id=None):
"""
商家发单时计算打手/管事分成。
打手优先:打手+管事 > 订单金额时,管事分成为 0。
"""
from jituan.constants import CLUB_ID_DEFAULT
jine = Decimal(str(jine))
try:
lilu_obj = CommissionRate.objects.get(Platform='3')
rate_dashou = Decimal(str(lilu_obj.Rate)) if lilu_obj.Rate and lilu_obj.Rate > 0 else Decimal('1')
except CommissionRate.DoesNotExist:
cid = club_id or CLUB_ID_DEFAULT
rate_dashou = get_commission_rate(cid, '3')
if not rate_dashou or rate_dashou <= 0:
rate_dashou = Decimal('1')
lilu_guanshi_obj = CommissionRate.objects.filter(Platform='13').first()
rate_guanshi = (
Decimal(str(lilu_guanshi_obj.Rate))
if lilu_guanshi_obj and lilu_guanshi_obj.Rate is not None
else Decimal('0')
)
rate_guanshi = get_commission_rate(cid, '13')
dashou_fencheng = (jine * rate_dashou).quantize(Decimal('0.01'))
guanshi_raw = (jine * rate_guanshi).quantize(Decimal('0.01'))
@@ -182,6 +174,7 @@ def settle_shangjia_order_guanshi_fenhong(order, dashou_id):
avatar=user_main.Avatar if user_main else None,
nicheng=dashou.nicheng or '未知打手',
fenhong_leixing=3,
club_id=resolve_gsfenhong_club_id(dingdan_id=order.OrderID, dashouid=dashou_id, order=order),
)
logger.info(
f"商家订单管事分红成功: 订单{order.OrderID}, 管事{guanshi_id}, "

View File

@@ -56,17 +56,18 @@ from orders.notice_tasks import dingdan_guangbo
from .models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyBonus
)
from orders.models import (
CommissionRate, PlayerRating, RefundRecord
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyBonus,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_penalty import resolve_penalty_club_id
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
Szjilu, ShangjiaLianjie
ShangjiaLianjie
)
@@ -133,8 +134,11 @@ class WechatPayNotifyView(APIView):
out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else ''
transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else ''
from jituan.services.wechat_pay import club_id_from_order, verify_wechat_v2_signature
notify_club_id = club_id_from_order(out_trade_no)
# 3. 签名验证
if not self._verify_signature(xml_data):
if not verify_wechat_v2_signature(xml_data, notify_club_id):
logger.warning("签名验证失败")
return self._gen_response_xml('FAIL', '签名验证失败')
@@ -303,10 +307,10 @@ class WechatPayNotifyView(APIView):
# 更新收支记录(总累计)
self._update_szjilu(dingdan.Amount)
self._update_szjilu(dingdan.Amount, getattr(dingdan, 'ClubID', None))
# 更新每日收入统计
update_daily_income(dingdan.Amount)
update_daily_income(dingdan.Amount, getattr(dingdan, 'ClubID', None))
# 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用)
order_info = {
@@ -346,28 +350,12 @@ class WechatPayNotifyView(APIView):
logger.error(f"处理支付失败异常: {e}")
logger.error(traceback.format_exc())
def _update_szjilu(self, jine):
"""更新收支记录表(ID=1的记录"""
def _update_szjilu(self, jine, club_id=None):
"""更新收支记录表(按俱乐部"""
from jituan.services.szjilu_accounting import apply_szjilu_income
try:
jine_decimal = Decimal(str(jine))
with transaction.atomic():
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
if created:
logger.info("创建ID=1的收支记录")
szjilu.TotalIncome += jine_decimal
szjilu.TotalFlow += jine_decimal
szjilu.DailyFlow += jine_decimal
szjilu.save()
logger.info(f"收支记录更新成功,增加金额: {jine}")
apply_szjilu_income(jine, club_id)
logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}")
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
@@ -580,10 +568,10 @@ class PaymentVerifyView(APIView):
# 更新收支记录
self._update_szjilu(dingdan.Amount)
self._update_szjilu(dingdan.Amount, getattr(dingdan, 'ClubID', None))
# 更新每日收入统计
update_daily_income(dingdan.Amount)
update_daily_income(dingdan.Amount, getattr(dingdan, 'ClubID', None))
def _query_wechat_payment(self, out_trade_no):
"""
@@ -620,28 +608,12 @@ class PaymentVerifyView(APIView):
err_msg = result.get('return_msg', '未知错误')
raise Exception(f"微信订单查询失败: {err_msg}")
def _update_szjilu(self, jine):
"""更新收支记录表(ID=1的记录"""
def _update_szjilu(self, jine, club_id=None):
"""更新收支记录表(按俱乐部"""
from jituan.services.szjilu_accounting import apply_szjilu_income
try:
jine_decimal = Decimal(str(jine))
with transaction.atomic():
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
if created:
logger.info("创建ID=1的收支记录")
szjilu.TotalIncome += jine_decimal
szjilu.TotalFlow += jine_decimal
szjilu.DailyFlow += jine_decimal
szjilu.save()
logger.info(f"收支记录更新成功,增加金额: {jine}")
apply_szjilu_income(jine, club_id)
logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}")
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
# 不抛出异常,因为收支记录不是核心流程,但记录错误供排查
@@ -738,8 +710,8 @@ class CreateOrderView(APIView):
dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 查询利率并计算分成
# 查询打手利率(字符'1'
dashou_lilu = CommissionRate.query.filter(Platform='1').first()
club_id = resolve_club_id_from_request(request)
dashou_lilu = get_commission_rate_object(club_id, '1')
# 🔥 新增:判断是否使用商品配置的额外分成
@@ -754,12 +726,10 @@ class CreateOrderView(APIView):
# 查询管事利率(字符'2'
#guanshi_lilu = CommissionRate.query.filter(Platform='2').first()
if not dashou_lilu: #or not guanshi_lilu:
if not (shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None):
if get_commission_rate(club_id, '1') <= 0 and not CommissionRate.query.filter(Platform='1').exists():
return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
# 计算打手分成
#dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate))
# 开始数据库事务
with transaction.atomic():
@@ -788,7 +758,8 @@ class CreateOrderView(APIView):
'Nickname': nicheng,
'ImageURL':shangpin_tupian,
'User1ID':f"Boss{request.user.UserUID}",
'ProductTypeID':shangpin.leixing_id
'ProductTypeID':shangpin.leixing_id,
'ClubID': getattr(request, 'club_id', None) or 'xq',
}
# 创建订单记录
@@ -1943,7 +1914,8 @@ class ShangjiaPaifaView(APIView):
return Response({'code': 400, 'msg': '指定用户不是打手'})
# ----- 10. 利率(打手+管事分成,打手优先) -----
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage)
club_id = resolve_club_id_from_request(request)
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
# ----- 11. 生成订单ID -----
def generate_dingdan_id():
@@ -1973,7 +1945,8 @@ class ShangjiaPaifaView(APIView):
Remark=dingdan_beizhu[:1000],
Nickname=laoban_name[:50],
ImageURL=request.user.Avatar,
User1ID=f"Sj{request.user.UserUID}"
User1ID=f"Sj{request.user.UserUID}",
ClubID=club_id,
)
logger.info(f"订单主表创建: {dingdan.OrderID}")
@@ -5579,19 +5552,11 @@ class AdTongYiTuiKuanPingTai(APIView):
except (User.DoesNotExist, AttributeError):
logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过")
# 16. 更新收支记录表(id=1
# 16. 更新收支记录表(按俱乐部
try:
update_daily_payout(jine)
szjilu_obj = Szjilu.query.get(id=1)
szjilu_obj.TotalIncome -= jine
szjilu_obj.TotalExpense += jine
szjilu_obj.DailyExpense += jine
szjilu_obj.save()
except Szjilu.DoesNotExist:
logger.error(f"平台退款:收支记录表(id=1)不存在,跳过")
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(jine, getattr(dingdan_obj, 'ClubID', None))
apply_szjilu_expense(jine, getattr(dingdan_obj, 'ClubID', None))
except Exception as e:
logger.error(f"更新收支记录表异常: {str(e)}")
@@ -6450,6 +6415,7 @@ class ShangjiaFakuanApplyView(APIView):
Status=1, # 待缴纳
AffectsGrabbing=yingxiang_qiangdan,
ApplicantIdentity=5, # 申请人是商家
ClubID=resolve_penalty_club_id(order=order, penalized_user_id=dashou_id),
)
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}")
return Response({'code': 0, 'msg': '罚款申请已提交'})

View File

@@ -0,0 +1,25 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='czjilu',
name='club_id',
field=models.CharField(
db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部',
),
),
migrations.AddField(
model_name='huiyuangoumai',
name='club_id',
field=models.CharField(
db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,18 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0003_czjilu_huiyuangoumai_club_id'),
]
operations = [
migrations.AddField(
model_name='gsfenhong',
name='club_id',
field=models.CharField(
db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,20 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0004_gsfenhong_club_id'),
]
operations = [
migrations.AddField(
model_name='duocifenhong',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
),
migrations.AlterUniqueTogether(
name='duocifenhong',
unique_together={('club_id', 'huiyuan', 'yonghuid', 'cishu')},
),
]

View File

@@ -169,6 +169,9 @@ class Huiyuangoumai(QModel):
daoqi_time = models.DateTimeField(verbose_name='会员到期时间')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
class Meta:
db_table = 'huiyuangoumai'
@@ -212,6 +215,9 @@ class Czjilu(QModel):
jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单金额')
leixing = models.IntegerField(null=True, blank=True, verbose_name='充值类型1:会员2:押金,3:积分,4:商家')
shuoming = models.CharField(max_length=100, null=True, blank=True, verbose_name='订单说明')
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
@@ -249,8 +255,9 @@ class Gsfenhong(QModel):
)
huiyuan_id = models.CharField(max_length=6, null=True, blank=True, verbose_name='会员ID')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
club_id = models.CharField(
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
)
class Meta:
db_table = 'gsfenhong'
@@ -275,6 +282,7 @@ class DuociFenhong(QModel):
"""
huiyuan = models.CharField(max_length=10, verbose_name='会员ID')
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事ID')
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部')
cishu = models.PositiveIntegerField(verbose_name='分红次数')
guanshi_fenhong = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='管事分红金额')
@@ -286,7 +294,7 @@ class DuociFenhong(QModel):
db_table = 'duoci_fenhong'
verbose_name = '多次分红配置'
verbose_name_plural = '多次分红配置'
unique_together = ('huiyuan', 'yonghuid', 'cishu') # 同一管事、同一会员、同一次数唯一
unique_together = ('club_id', 'huiyuan', 'yonghuid', 'cishu')
indexes = [
models.Index(fields=['yonghuid']),

View File

@@ -51,9 +51,18 @@ from users.models import (
from users.business_models import User
from orders.models import CommissionRate
from config.models import Szjilu, Gonggao, Lunbo
from config.models import Gonggao, Lunbo
from orders.models import Penalty # 用于罚款类型
from jituan.services.club_config import (
get_commission_rate,
get_commission_rate_object,
get_huiyuan_price,
get_huiyuan_fenchong,
)
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.club_penalty import resolve_gsfenhong_club_id
import traceback
@@ -258,6 +267,7 @@ class DashouHuiyuanList(APIView):
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 查询所有会员数据ORM查询确保性能
club_id = resolve_club_id_from_request(request)
huiyuan_queryset = Huiyuan.query.all().only(
'huiyuan_id', # 会员ID
'jieshao', # 会员名字注意jieshao字段实际存储的是会员名字
@@ -271,7 +281,7 @@ class DashouHuiyuanList(APIView):
huiyuan_list.append({
'id': huiyuan.huiyuan_id, # 会员ID对应前端id
'mingzi': huiyuan.jieshao, # 会员名字对应前端mingzi
'jiage': float(huiyuan.jiage), # 会员价格(转为浮点数)
'jiage': float(get_huiyuan_price(club_id, huiyuan.huiyuan_id, huiyuan)),
'jieshao': huiyuan.jtjieshao # 会员介绍对应前端jieshao
})
@@ -357,7 +367,8 @@ class YajinGoumai(APIView):
yonghuid=request.user.UserUID,
jine=jine,
leixing=2, # 充值类型2=押金
shuoming=settings.YAJIN_PAY_DESCRIPTION
shuoming=settings.YAJIN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建押金充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}")
@@ -397,9 +408,12 @@ class YajinGoumai(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信小程序支付参数"""
# 获取配置
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -566,13 +580,16 @@ class YajinHuitiao(View):
logger.info(f"回调数据解析: 订单{out_trade_no}, 状态{return_code}/{result_code}, 金额{total_fee}")
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
# 2. 验证基本返回状态
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
logger.warning(f"微信支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not self.verify_signature(request_body):
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
@@ -650,6 +667,11 @@ class YajinHuitiao(View):
avatar=user_main.Avatar,
nicheng=dashou.nicheng or '未知打手',
fenhong_leixing=2,
club_id=resolve_gsfenhong_club_id(
dingdan_id=order.dingdan_id,
dashouid=order.yonghuid,
czjilu=order,
),
)
logger.info(
f"押金分红成功: 订单{order.dingdan_id}, 管事{guanshi_id}, "
@@ -684,25 +706,9 @@ class YajinHuitiao(View):
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
# 查询或创建ID=1的收支记录
update_daily_income(jine_decimal)
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
@@ -953,7 +959,8 @@ class JifenBuchong(APIView):
yonghuid=request.user.UserUID,
jine=jine,
leixing=3, # 充值类型3=积分
shuoming=settings.JIFEN_PAY_DESCRIPTION
shuoming=settings.JIFEN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建积分充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, 金额{jine}")
@@ -993,9 +1000,12 @@ class JifenBuchong(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -1166,7 +1176,9 @@ class JifenHuitiao(View):
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名
if not self.verify_signature(request_body):
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"积分签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
@@ -1198,26 +1210,11 @@ class JifenHuitiao(View):
try:
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
@@ -1514,7 +1511,8 @@ class HuiyuanGoumai(APIView):
dingdanid = f"CzHY{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
# 6. 创建充值记录订单
jine = float(huiyuan.jiage) # 会员价格
club_id = resolve_club_id_from_request(request)
jine = float(get_huiyuan_price(club_id, huiyuanid, huiyuan))
# 组合订单说明:会员介绍 + 配置描述
shuoming = f"{huiyuan.jieshao} - {settings.HUIYUAN_PAY_DESCRIPTION}"
@@ -1525,7 +1523,8 @@ class HuiyuanGoumai(APIView):
jine=jine,
leixing=1, # 充值类型1=会员
shuoming=shuoming,
huiyuan_id=huiyuanid # 存储会员ID
huiyuan_id=huiyuanid, # 存储会员ID
club_id=club_id,
)
logger.info(
@@ -1566,9 +1565,12 @@ class HuiyuanGoumai(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信支付参数(完整版,无省略)"""
# 微信支付配置 - 从settings中获取
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'yajin':
@@ -1737,7 +1739,9 @@ class HuiyuanHuitiao(View):
logger.warning(f"会员支付回调状态异常: {return_code}/{result_code}")
return self.wechat_response('FAIL', '支付失败')
if not self.verify_signature(request_body):
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"会员签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
@@ -1774,17 +1778,9 @@ class HuiyuanHuitiao(View):
try:
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
szjilu, _ = Szjilu.query.get_or_create(
id=1,
defaults={'TotalIncome': Decimal('0.00'), 'TotalFlow': Decimal('0.00'), 'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'), 'DailyFlow': Decimal('0.00')}
)
szjilu.TotalIncome += jine_decimal
szjilu.TotalFlow += jine_decimal
szjilu.DailyFlow += jine_decimal
szjilu.save()
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
@@ -1851,7 +1847,8 @@ class HuiyuanHuitiao(View):
huiyuan_id=huiyuan_id,
jieshao=huiyuan.jieshao,
huiyuan_zhuangtai=1,
daoqi_time=timezone.now() + timedelta(days=30)
daoqi_time=timezone.now() + timedelta(days=30),
club_id=get_user_club_id(User.query.filter(UserUID=yonghuid).first()),
)
logger.info(f"新购会员: 用户{yonghuid}, 会员{huiyuan_id}")
return goumai_record, True, is_first_buy_any
@@ -1911,9 +1908,13 @@ class HuiyuanHuitiao(View):
# 获取订单金额(用于限制分红总额)
order_amount = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
club_id = getattr(order, 'club_id', None) or resolve_club_id_for_write(user=dashou.user)
guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan.huiyuan_id, huiyuan)
# 计算管事分红金额
guanshi_fenhong = self._calc_guanshi_fenhong(guanshi, huiyuan, guanshi_id, cishu)
guanshi_fenhong = self._calc_guanshi_fenhong(
guanshi, huiyuan, guanshi_id, cishu, default_fc=guanshi_fc,
)
if guanshi_fenhong is None:
logger.info(f"管事{guanshi_id}本次无分红")
guanshi_fenhong = Decimal('0.00')
@@ -1928,7 +1929,9 @@ class HuiyuanHuitiao(View):
logger.warning(f"组长信息不存在: {zuzhang_id}")
zuzhang = None
zuzhang_fenhong = self._calc_zuzhang_fenhong(zuzhang, huiyuan, zuzhang_id, cishu)
zuzhang_fenhong = self._calc_zuzhang_fenhong(
zuzhang, huiyuan, zuzhang_id, cishu, default_fc=zuzhang_fc,
)
if zuzhang_fenhong is None:
logger.info(f"组长{zuzhang_id}本次无分红")
zuzhang_fenhong = Decimal('0.00')
@@ -1975,11 +1978,15 @@ class HuiyuanHuitiao(View):
fenhong=guanshi_fenhong,
avatar=avatar,
nicheng=nicheng,
# 新增字段
zuzhang_id=zuzhang_id if zuzhang_fenhong > 0 else None,
zuzhang_fenhong=zuzhang_fenhong if zuzhang_fenhong > 0 else None,
huiyuan_id=huiyuan.huiyuan_id,
fenhong_leixing=1,
club_id=resolve_gsfenhong_club_id(
dingdan_id=order.dingdan_id,
dashouid=order.yonghuid,
czjilu=order,
),
)
@@ -2028,22 +2035,24 @@ class HuiyuanHuitiao(View):
logger.error(f"处理分红失败: {str(e)}", exc_info=True)
# 不抛出异常,避免影响主流程
def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu):
def _calc_guanshi_fenhong(self, guanshi, huiyuan, guanshi_id, cishu, default_fc=None):
"""计算管事分红金额"""
# 先查多次分红配置表
try:
cid = resolve_gsfenhong_club_id(dashouid=guanshi_id)
config = DuociFenhong.query.get(
club_id=cid,
huiyuan=huiyuan,
yonghuid=guanshi_id,
cishu=cishu
cishu=cishu,
)
return Decimal(str(config.guanshi_fenhong))
except DuociFenhong.DoesNotExist:
pass
if cishu == 1:
# 第一次分红,取会员表默认金额
return Decimal(str(huiyuan.guanshifc))
base = default_fc if default_fc is not None else huiyuan.guanshifc
return Decimal(str(base))
else:
# 第二次及以上,检查是否开启二次分红
if guanshi.fenghong_erci_enabled:
@@ -2051,24 +2060,26 @@ class HuiyuanHuitiao(View):
else:
return None # 无分红
def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu):
def _calc_zuzhang_fenhong(self, zuzhang, huiyuan, zuzhang_id, cishu, default_fc=None):
"""计算组长分红金额"""
if zuzhang is None:
return None
# 查多次分红配置表
try:
cid = resolve_gsfenhong_club_id(dashouid=zuzhang_id)
config = DuociFenhong.query.get(
club_id=cid,
huiyuan=huiyuan,
yonghuid=zuzhang_id,
cishu=cishu
cishu=cishu,
)
return Decimal(str(config.zuzhang_fenhong))
except DuociFenhong.DoesNotExist:
pass
if cishu == 1:
# 第一次分红,取会员表默认组长分成
return Decimal(str(huiyuan.zuzhangfc))
base = default_fc if default_fc is not None else huiyuan.zuzhangfc
return Decimal(str(base))
else:
# 第二次及以上,检查组长是否开启额外分红
if zuzhang.kaioi_ewai_fenhong:
@@ -2358,7 +2369,8 @@ class ShangjiaChongzhi(APIView):
yonghuid=request.user.UserUID,
jine=jine,
leixing=4, # 充值类型4=商家充值
shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION
shuoming=settings.SHANGJIA_CZ_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
logger.info(f"创建商家充值订单: 商家{shangjia.nicheng}, 订单{dingdanid}, 金额{jine}")
except Exception as e:
@@ -2404,9 +2416,12 @@ class ShangjiaChongzhi(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
"""生成微信小程序支付参数"""
# 获取配置
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
# 根据支付类型获取对应的配置
if pay_type == 'shangjia':
@@ -2573,7 +2588,9 @@ class ShangjiaHuitiao(View):
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not self.verify_signature(request_body):
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"商家支付签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
@@ -2606,26 +2623,11 @@ class ShangjiaHuitiao(View):
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
@@ -4361,13 +4363,10 @@ class AdShangpinHuQu(APIView):
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询公告数据(类型=1的公告
gonggao_obj = Gonggao.query.filter(NoticeType=1).first()
shangpingonggao = gonggao_obj.Content if gonggao_obj else ''
# 7. 查询轮播图数据(类型=1的轮播图
lunbo_objs = Lunbo.query.filter(ImageType=1).values('ImageURL')
shangpinlunbo = [item['ImageURL'] for item in lunbo_objs]
# 6. 查询公告与轮播(按俱乐部,默认抢单池
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls
shangpingonggao = get_gonggao_content(request, notice_type=1)
shangpinlunbo = get_lunbo_urls(request, page_key='order_pool', image_type=1)
# 8. 查询商品类型数据
shangpinleixing_data = ShangpinLeixing.query.all().values(
@@ -6077,7 +6076,8 @@ class CzhqdyView(APIView):
# 会员
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
target_price = huiyuan.jiage
club_id = resolve_club_id_from_request(request)
target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
@@ -6096,12 +6096,9 @@ class CzhqdyView(APIView):
# 获取所有费率(打手佣金、管事分红、组长分红、打手押金)
rates = {}
club_id = resolve_club_id_from_request(request)
for key in ['5', '6', '8', '11']:
try:
rate_obj = CommissionRate.query.get(Platform=key)
rates[key] = rate_obj.Rate
except CommissionRate.DoesNotExist:
rates[key] = None # 未配置则跳过
rates[key] = get_commission_rate(club_id, key)
# 构建可用身份列表
available_list = []
@@ -6214,10 +6211,11 @@ class DsqrgmdhView(APIView):
return Response({'code': 400, 'msg': 'yajin_jine 必须为正数'}, status=status.HTTP_400_BAD_REQUEST)
target_price = None
club_id = resolve_club_id_from_request(request)
if leixing == 1:
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
target_price = huiyuan.jiage
target_price = get_huiyuan_price(club_id, huiyuan_id, huiyuan)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员不存在'}, status=status.HTTP_404_NOT_FOUND)
elif leixing == 2:
@@ -6302,6 +6300,7 @@ class DsqrgmdhView(APIView):
jine=required,
leixing=leixing,
shuoming=shuoming,
club_id=club_id,
)
response_data = {}
@@ -6394,7 +6393,8 @@ class DsqrgmdhView(APIView):
huiyuan_id=huiyuan_id,
jieshao=huiyuan_obj.jieshao,
huiyuan_zhuangtai=1,
daoqi_time=timezone.now() + timedelta(days=30)
daoqi_time=timezone.now() + timedelta(days=30),
club_id=get_user_club_id(User.query.filter(UserUID=yonghuid).first()),
)
return record, is_first_buy_any
except Exception as e:

View File

@@ -2611,7 +2611,7 @@ class ShopRefundView(APIView):
# 更新平台支出记录
try:
update_daily_payout(order.Amount)
update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
except Exception as e:
logger.error(f"更新每日支出失败: {e}")

View File

@@ -43,6 +43,10 @@ class User(_UserBase):
IP = models.CharField(max_length=50, db_column='IP', verbose_name='来访用户IP', null=True, blank=True)
IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工')
IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户')
ClubID = models.CharField(
max_length=16, default='xq', db_column='ClubID', verbose_name='所属俱乐部',
null=True, blank=True,
)
objects = UserManager()

View File

@@ -64,7 +64,7 @@ def process_fadan_fenhong(fadan_id):
return True, "罚款金额为0无分红"
# 5. 确定分红利率
rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen)
rate = _get_fenhong_rate(shenqing_chufa, fenhong_shenfen, lilv_shenfen, club_id=fadan.ClubID)
if rate is None:
logger.warning(f"未找到身份 {src_shenfen} (映射后 fenhong_shenfen={fenhong_shenfen}, lilv_shenfen={lilv_shenfen}) 的分红利率,罚单 {fadan_id} 不进行分红")
fadan.ApplicantBonusAmount = Decimal('0.00')
@@ -79,6 +79,8 @@ def process_fadan_fenhong(fadan_id):
fadan.save(update_fields=['ApplicantBonusAmount'])
return True, "分红金额为0"
club_id = fadan.ClubID or 'xq'
# 7. 根据申请处罚者身份执行不同的分红逻辑
# src_shenfen == 5 代表商家(源身份)
if src_shenfen == 5:
@@ -87,6 +89,7 @@ def process_fadan_fenhong(fadan_id):
fenhong_record, created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
defaults={
'TotalAmount': Decimal('0.00'),
'WithdrawableAmount': Decimal('0.00'),
@@ -120,6 +123,7 @@ def process_fadan_fenhong(fadan_id):
fenhong_record, created = PenaltyBonus.objects.get_or_create(
BeneficiaryID=shenqing_chufa,
Identity=fenhong_shenfen,
ClubID=club_id,
defaults={
'TotalAmount': Decimal('0.00'),
'WithdrawableAmount': Decimal('0.00'),
@@ -145,7 +149,7 @@ def process_fadan_fenhong(fadan_id):
return False, f"分红处理异常: {str(e)}"
def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen):
def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen, club_id='xq'):
"""
获取分红利率(优先使用个人单独配置,否则使用全局身份配置)
"""
@@ -153,6 +157,7 @@ def _get_fenhong_rate(fenhongzhe_id, fenhong_shenfen, lilv_shenfen):
individual = PenaltyBonus.objects.get(
BeneficiaryID=fenhongzhe_id,
Identity=fenhong_shenfen,
ClubID=club_id,
is_individual_rate=True
)
return individual.IndividualRate

View File

@@ -0,0 +1,20 @@
"""users: User.ClubID 字段"""
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='ClubID',
field=models.CharField(
blank=True, db_column='ClubID', default='xq',
max_length=16, null=True, verbose_name='所属俱乐部',
),
),
]

View File

@@ -0,0 +1,30 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0002_user_clubid'),
]
operations = [
migrations.AddField(
model_name='tixianautorecord',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='tixianjilu',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddField(
model_name='tixianshenhejilu',
name='club_id',
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
),
migrations.AddIndex(
model_name='tixianjilu',
index=models.Index(fields=['club_id', 'zhuangtai'], name='tixianjilu_club_status_idx'),
),
]

View File

@@ -340,6 +340,7 @@ class UserKefu(QModel):
class Tixianjilu(QModel):
id = models.AutoField(primary_key=True, verbose_name='提现记录ID')
club_id = models.CharField(max_length=16, default='xq', db_index=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
phone = models.CharField(max_length=11, blank=True, null=True, verbose_name='手机号')
@@ -370,6 +371,7 @@ class Tixianjilu(QModel):
verbose_name = '提现记录表'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['club_id', 'zhuangtai']),
models.Index(fields=['yonghuid']),
models.Index(fields=['zhuangtai']),
models.Index(fields=['shenheid']),
@@ -393,6 +395,7 @@ class TixianShenheJilu(QModel):
shenhe_danhao = models.CharField(
max_length=32, unique=True, db_index=True, verbose_name='审核单号'
)
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
# 提现类型1打手佣金 2管事分红 3组长分红 4考核官分佣 5打手押金 6商家余额
leixing = models.PositiveSmallIntegerField(db_index=True, verbose_name='提现类型')
@@ -454,6 +457,7 @@ class TixianAutoRecord(QModel):
db_index=True,
verbose_name='提现订单号(自定义)'
)
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID')
leixing = models.IntegerField(verbose_name='提现类型 1打手 2管事')
jine = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='申请提现金额')

View File

@@ -25,8 +25,15 @@ from django.conf import settings
from django.db import transaction
from django.utils import timezone
from orders.models import Order, MerchantOrderExt, Penalty, CommissionRate
from config.models import Szjilu, TixianQuotaDefault
from orders.models import Order, MerchantOrderExt, Penalty
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
from jituan.services.withdraw_config import (
get_or_create_platform_daily_stat,
get_personal_daily_quota,
get_platform_daily_limit,
get_platform_daily_total,
get_tixian_feilv as _get_tixian_feilv_for_club,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
@@ -67,6 +74,19 @@ LEIXING_RATE_KEY = {
6: '10',
}
def _club_id_for_user(user_main):
return club_id_for_tixian_yonghuid(user_main.UserUID)
def get_tixian_feilv(leixing, yonghuid=None, club_id=None):
"""按用户所属俱乐部读取提现手续费率。"""
if club_id is None:
if not yonghuid:
raise ValueError('缺少用户或俱乐部')
club_id = club_id_for_tixian_yonghuid(yonghuid)
return _get_tixian_feilv_for_club(club_id, leixing)
DAKUAN_MODE_MANUAL = 1
DAKUAN_MODE_AUTO = 2
@@ -149,16 +169,6 @@ def generate_shenhe_danhao():
raise RuntimeError('生成审核单号失败,请重试')
def get_tixian_feilv(leixing):
rate_key = LEIXING_RATE_KEY.get(leixing)
if not rate_key:
raise ValueError('无效的提现类型')
rate_obj = CommissionRate.query.filter(Platform=rate_key).first()
if not rate_obj:
raise ValueError(f'提现费率未配置(类型{leixing}')
return decimal.Decimal(str(rate_obj.Rate))
def calc_fee_amounts(jine, feilv):
"""申请金额 jine实际到账 = jine - 手续费"""
shouxufei = (jine * feilv).quantize(decimal.Decimal('0.01'))
@@ -410,25 +420,8 @@ def validate_collect_eligibility(user_main, leixing):
return False, '无效的提现类型'
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.query.get(UserType=1).default_quota
if leixing == 2:
if profile.kaioi_ewai_xiane and profile.ewai_xiane > 0:
return profile.ewai_xiane
return TixianQuotaDefault.query.get(UserType=2).default_quota
if leixing == 3:
try:
default_quota = TixianQuotaDefault.query.get(UserType=3).default_quota
except TixianQuotaDefault.DoesNotExist:
default_quota = decimal.Decimal('0.00')
if profile.kaioi_ewai_tixian and profile.ewai_tixian_xiane > 0:
return profile.ewai_tixian_xiane
return default_quota
raise ValueError('无效的个人限额身份')
def _get_personal_daily_quota(club_id, leixing, profile):
return get_personal_daily_quota(club_id, leixing, profile)
def _get_active_auto_record(shenhe_danhao):
@@ -444,23 +437,12 @@ def _get_active_auto_record(shenhe_danhao):
)
def _get_platform_daily_total(leixing, today=None):
"""平台今日已提现总额:只读 houtai.WithdrawalDailyStats到账金额口径"""
if today is None:
today = date.today()
try:
return WithdrawalDailyStats.query.get(Date=today, WithdrawalType=leixing).total_amount
except WithdrawalDailyStats.DoesNotExist:
return decimal.Decimal('0.00')
def _get_platform_daily_total(club_id, leixing, today=None):
return get_platform_daily_total(club_id, leixing, today)
def _calc_platform_projected(leixing, shijidaozhang, shenhe_danhao=''):
"""
平台今日预占后总额(收款前只读预估)。
本审核单若已有进行中打款记录,其金额尚未写入统计表,但不应重复加计。
返回 (daily_used, projected_total, additional_amount)
"""
daily_used = _get_platform_daily_total(leixing)
def _calc_platform_projected(club_id, leixing, shijidaozhang, shenhe_danhao=''):
daily_used = _get_platform_daily_total(club_id, leixing)
active_rec = _get_active_auto_record(shenhe_danhao)
if active_rec:
return daily_used, daily_used, decimal.Decimal('0.00')
@@ -475,20 +457,14 @@ def _projected_personal_total(personal_today, jine, shenhe_danhao=''):
return personal_today - already_counted + jine
def _get_platform_daily_limit(leixing):
"""
平台每日总限额peizhi.TixianQuotaDefault
提现身份1打手→配置42管事→53组长→64考核官→75押金→86商家→9
"""
quota_leixing = leixing + 3
try:
return TixianQuotaDefault.query.get(UserType=quota_leixing).default_quota
except TixianQuotaDefault.DoesNotExist:
def _get_platform_daily_limit(club_id, leixing):
limit = get_platform_daily_limit(club_id, leixing)
if limit <= 0:
logger.warning(
'平台每日总限额未配置 leixing=%s(读取quota_leixing=%s)按0处理禁止新增收款',
leixing, quota_leixing,
'平台每日总限额为0 club_id=%s leixing=%s',
club_id, leixing,
)
return decimal.Decimal('0.00')
return limit
def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''):
@@ -525,7 +501,7 @@ def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''):
else:
personal_today = decimal.Decimal('0.00')
personal_quota = _get_personal_daily_quota(leixing, profile)
personal_quota = _get_personal_daily_quota(_club_id_for_user(user_main), leixing, profile)
projected_personal = _projected_personal_total(personal_today, jine, shenhe_danhao)
if projected_personal > personal_quota:
return (
@@ -536,22 +512,22 @@ def _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao=''):
return True, '', ''
def _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao=''):
def _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao=''):
"""平台每日总限额只读校验:今日已用 + 本笔到账 > 总限额 则拒绝"""
platform_limit = _get_platform_daily_limit(leixing)
platform_limit = _get_platform_daily_limit(club_id, leixing)
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
if platform_limit <= 0 and shijidaozhang > 0:
return False, '今日该角色提现已关闭总限额为0暂无法收款', 'platform'
daily_used, projected, additional = _calc_platform_projected(
leixing, shijidaozhang, shenhe_danhao,
club_id, leixing, shijidaozhang, shenhe_danhao,
)
if projected > platform_limit:
logger.warning(
'平台总限额拦截 leixing=%s shenhe_danhao=%s daily_used=%s additional=%s '
'平台总限额拦截 club_id=%s leixing=%s shenhe_danhao=%s daily_used=%s additional=%s '
'projected=%s limit=%s',
leixing, shenhe_danhao, daily_used, additional, projected, platform_limit,
club_id, leixing, shenhe_danhao, daily_used, additional, projected, platform_limit,
)
return (
False,
@@ -570,7 +546,8 @@ def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_d
ok, msg, kind = _check_personal_quota_readonly(user_main, leixing, jine, shenhe_danhao)
if not ok:
return False, msg, kind
return _check_platform_quota_readonly(leixing, shijidaozhang, shenhe_danhao)
club_id = _club_id_for_user(user_main)
return _check_platform_quota_readonly(club_id, leixing, shijidaozhang, shenhe_danhao)
def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''):
@@ -583,6 +560,7 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
club_id = _club_id_for_user(user_main)
if _get_active_auto_record(shenhe_danhao):
return True, '', ''
@@ -609,7 +587,7 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
personal_profile.jinri_tixian = decimal.Decimal('0.00')
personal_today = personal_profile.jinri_tixian
personal_quota = _get_personal_daily_quota(leixing, personal_profile)
personal_quota = _get_personal_daily_quota(club_id, leixing, personal_profile)
if personal_today + jine > personal_quota:
return (
False,
@@ -617,12 +595,8 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
'personal',
)
platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create(
Date=today,
WithdrawalType=leixing,
defaults={'total_amount': decimal.Decimal('0.00')},
)
platform_limit = _get_platform_daily_limit(leixing)
platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today)
platform_limit = _get_platform_daily_limit(club_id, leixing)
if platform_limit <= 0 and shijidaozhang > 0:
return False, '今日该角色提现已关闭总限额为0暂无法收款', 'platform'
if platform_stat.total_amount + shijidaozhang > platform_limit:
@@ -659,6 +633,7 @@ def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
"""微信打款明确失败/用户取消时回滚收款预占的限额"""
today = date.today()
shijidaozhang = shijidaozhang or decimal.Decimal('0.00')
club_id = _club_id_for_user(user_main)
try:
with transaction.atomic():
if leixing == 1:
@@ -679,7 +654,7 @@ def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
try:
stat = WithdrawalDailyStats.objects.select_for_update().get(
Date=today, WithdrawalType=leixing,
club_id=club_id, Date=today, WithdrawalType=leixing,
)
stat.total_amount = max(
stat.total_amount - shijidaozhang,
@@ -727,8 +702,10 @@ 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)
club_id = club_id_for_tixian_yonghuid(auto_record.yonghuid)
stat_total = _get_platform_daily_total(club_id, auto_record.leixing, today)
other_sum = TixianAutoRecord.query.filter(
club_id=club_id,
leixing=auto_record.leixing,
zhuangtai__in=[0, 1],
CreateTime__date=today,
@@ -758,11 +735,8 @@ def apply_transfer_success_quota(auto_record):
logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid)
return
platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create(
Date=today,
WithdrawalType=leixing,
defaults={'total_amount': decimal.Decimal('0.00')},
)
club_id = _club_id_for_user(user_main)
platform_stat = get_or_create_platform_daily_stat(club_id, leixing, today)
platform_stat.total_amount += shijidaozhang
platform_stat.save(update_fields=['total_amount'])
@@ -981,19 +955,28 @@ def close_audit_over_limit_refund(audit, auto_record=None, fail_reason='单笔
return True
def query_wechat_transfer_bill(out_bill_no):
def query_wechat_transfer_bill(out_bill_no, club_id=None):
"""
向微信查转账单状态
GET /v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}
返回 dict网络/系统异常返回 None微信无此单返回 {'_not_found': True}
"""
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
if not club_id:
try:
rec = TixianAutoRecord.query.filter(tixian_id=out_bill_no).first()
if rec:
club_id = club_id_for_tixian_yonghuid(rec.yonghuid)
except Exception:
pass
url = (
'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/'
f'transfer-bills/out-bill-no/{out_bill_no}'
)
try:
auth = build_authorization('GET', url, '')
auth = build_authorization('GET', url, '', club_id=club_id)
headers = {
'Authorization': auth,
'Accept': 'application/json',
@@ -1030,21 +1013,11 @@ def _parse_package_info(raw):
def _apply_platform_accounting(auto_record):
"""平台收支统计(幂等由 mark_transfer_success 外层保证只调一次)"""
update_daily_payout(auto_record.shijidaozhang)
try:
szjilu = Szjilu.objects.select_for_update().get(id=1)
szjilu.TotalIncome -= auto_record.shijidaozhang
szjilu.TotalExpense += auto_record.shijidaozhang
szjilu.DailyExpense += auto_record.shijidaozhang
szjilu.save()
except Szjilu.DoesNotExist:
Szjilu.query.create(
id=1,
TotalIncome=-auto_record.shijidaozhang,
TotalExpense=auto_record.shijidaozhang,
DailyExpense=auto_record.shijidaozhang,
)
from jituan.services.szjilu_accounting import apply_szjilu_expense
from jituan.services.wechat_pay import club_id_for_tixian_yonghuid
payout_club = club_id_for_tixian_yonghuid(auto_record.yonghuid)
update_daily_payout(auto_record.shijidaozhang, payout_club)
apply_szjilu_expense(auto_record.shijidaozhang, payout_club)
def mark_transfer_success(auto_record, *, wechat_transfer_no=None, with_accounting=True):
@@ -1210,7 +1183,7 @@ def reconcile_shenhe_wechat_bills(shenhe_danhao):
if rec.zhuangtai == 1:
return RECONCILE_COMPLETED, {'msg': '该提现已完成,请勿重复操作'}
wx_data = query_wechat_transfer_bill(rec.tixian_id)
wx_data = query_wechat_transfer_bill(rec.tixian_id, club_id_for_tixian_yonghuid(rec.yonghuid))
if wx_data is None:
logger.error(
'对账查单不可用 shenhe_danhao=%s bill=%s',
@@ -1316,7 +1289,7 @@ def handle_post_transfer_failure(tixian_id, shenhe_danhao, err_code='', err_msg=
except TixianAutoRecord.DoesNotExist:
return RECONCILE_PENDING, '收款处理中,请稍后再试'
wx_data = query_wechat_transfer_bill(tixian_id)
wx_data = query_wechat_transfer_bill(tixian_id, club_id_for_tixian_yonghuid(ar.yonghuid))
if wx_data is None:
logger.error(
'发起转账失败且查单不可用 bill=%s code=%s msg=%s,保持待查单',
@@ -1377,7 +1350,7 @@ def create_audit_application(user_main, leixing, jine):
if not ok:
raise ValueError(msg)
feilv = get_tixian_feilv(leixing)
feilv = get_tixian_feilv(leixing, yonghuid=yonghuid)
shouxufei, shijidaozhang = calc_fee_amounts(jine, feilv)
if shijidaozhang <= 0:
raise ValueError('提现金额过低,扣除手续费后无实际到账')
@@ -1391,9 +1364,11 @@ def create_audit_application(user_main, leixing, jine):
with transaction.atomic():
nicheng = deduct_balance(user_main, leixing, jine) or nicheng
club_id = club_id_for_tixian_yonghuid(yonghuid)
audit_kwargs = {
'shenhe_danhao': shenhe_danhao,
'club_id': club_id,
'yonghuid': yonghuid,
'leixing': leixing,
'zhuangtai': 1,
@@ -1407,6 +1382,7 @@ def create_audit_application(user_main, leixing, jine):
audit = TixianShenheJilu.query.create(**audit_kwargs)
tixian_record = Tixianjilu.query.create(
club_id=club_id,
yonghuid=yonghuid,
avatar=user_main.Avatar or '',
phone=user_main.Phone or '',

View File

@@ -120,10 +120,10 @@ def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijid
}
def _call_wechat_transfer(body, wx_cfg):
def _call_wechat_transfer(body, wx_cfg, club_id=None):
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'
body_json = json.dumps(body, separators=(',', ':'))
auth = build_authorization('POST', url, body_json)
auth = build_authorization('POST', url, body_json, club_id=club_id)
headers = {
'Authorization': auth,
'Content-Type': 'application/json',
@@ -203,9 +203,13 @@ def process_audit_collect(request):
if not user_main.OpenID:
return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'})
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
from jituan.services.club_user import get_user_club_id
from jituan.services.wechat_pay import get_wechat_v3_config
payout_club_id = get_user_club_id(user_main)
wx_cfg = get_wechat_v3_config(payout_club_id)
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
logger.error('微信打款配置不完整 APPID=%s MCHID=%s', wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
logger.error('微信打款配置不完整 club=%s APPID=%s MCHID=%s', payout_club_id, wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
return Response({'code': 11, 'msg': '系统配置异常,请联系客服'})
ctx, err = resolve_collect_context(
@@ -295,8 +299,10 @@ def process_audit_collect(request):
quota_reserved = True
tixian_id = generate_tixian_id()
from jituan.services.club_user import get_user_club_id
TixianAutoRecord.query.create(
tixian_id=tixian_id,
club_id=get_user_club_id(user_main),
yonghuid=yonghuid,
leixing=leixing,
jine=jine,
@@ -314,7 +320,7 @@ def process_audit_collect(request):
)
try:
resp, wx_result = _call_wechat_transfer(body, wx_cfg)
resp, wx_result = _call_wechat_transfer(body, wx_cfg, payout_club_id)
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
package_info = wx_result.get('package_info')

View File

@@ -43,8 +43,6 @@ 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
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
@@ -83,7 +81,7 @@ from orders.models import (
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi, Szjilu
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
@@ -1327,6 +1325,15 @@ class DashouZhuceView(APIView):
'data': None
}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.invite_guard import assert_invite_same_club
ok, msg = assert_invite_same_club(request, guanshi_profile.user, current_user)
if not ok:
return Response({
'code': 403,
'message': msg,
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 4. 验证管事状态
# 5. 核心:在数据库事务中创建用户的所有身份
@@ -1590,25 +1597,20 @@ class TixianXinxiHuoquView(APIView):
# 从request.user获取当前用户
user_main = request.user
# 🔥【修改开始 - 使用Decimal字段名是Rate】
# 查询打手提现费率 (Platform='5')
dashou_rate_obj = CommissionRate.query.filter(Platform='5').first()
dashou_rate = decimal.Decimal(str(dashou_rate_obj.Rate)) if dashou_rate_obj else decimal.Decimal('0.00')
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import get_tixian_feilv
# 查询管事提现费率 (Platform='6')
guanshi_rate_obj = CommissionRate.query.filter(Platform='6').first()
guanshi_rate = decimal.Decimal(str(guanshi_rate_obj.Rate)) if guanshi_rate_obj else decimal.Decimal('0.00')
# 🔥【修改结束】
club_id = get_user_club_id(user_main)
dashou_rate = get_tixian_feilv(club_id, 1)
guanshi_rate = get_tixian_feilv(club_id, 2)
# 构建返回数据
response_data = {
'txdianhua': user_main.Phone if user_main.Phone else '',
'txtupian': user_main.PaymentQRCode if user_main.PaymentQRCode else '',
'txzh': user_main.PaymentAccount if user_main.PaymentAccount else '',
# 🔥【修改开始】
'dashou_rate': dashou_rate, # 打手提现费率
'guanshi_rate': guanshi_rate, # 管事提现费率
# 🔥【修改结束】
'dashou_rate': dashou_rate,
'guanshi_rate': guanshi_rate,
}
# 返回成功响应
@@ -1850,7 +1852,10 @@ class TixianShenqingView(APIView):
return Response({'code': 5, 'msg': '无效的收款方式只能是1(微信)或2(支付宝)'},
status=status.HTTP_400_BAD_REQUEST)
# 2. 获取对应费率及手续费
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import get_tixian_feilv
club_id = get_user_club_id(user_main)
current_rate = decimal.Decimal('0.00')
rate_key = {
1: '5', # 打手佣金
@@ -1862,10 +1867,9 @@ class TixianShenqingView(APIView):
}.get(leixing)
if rate_key:
rate_obj = CommissionRate.query.filter(Platform=rate_key).first()
if rate_obj:
current_rate = decimal.Decimal(str(rate_obj.Rate))
else:
try:
current_rate = get_tixian_feilv(club_id, leixing)
except ValueError:
return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing}'},
status=status.HTTP_400_BAD_REQUEST)
@@ -1956,7 +1960,9 @@ class TixianShenqingView(APIView):
phone = user_main.Phone or ''
zhifu = user_main.PaymentQRCode or ''
skzhanghao = user_main.PaymentAccount or ''
from jituan.services.club_user import get_user_club_id
tixian_record = Tixianjilu.query.create(
club_id=get_user_club_id(user_main),
yonghuid=yonghuid,
avatar=avatar,
phone=phone,
@@ -2482,43 +2488,10 @@ class AdminFinancialDataView(APIView):
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询收支记录表(Szjilu
# 使用高效的ORM查询获取最新的一条记录
# 注意:根据你的业务需求,这里获取最新的一条记录
# 如果需要汇总多条记录可以使用aggregate进行聚合查询
# 方法1获取最新的一条记录假设表中只有一条汇总记录
shouzhijilu = Szjilu.query.order_by('-CreateTime').first()
# 方法2如果表中可能有多条记录需要汇总可以使用以下方式
# from django.db.models Sum
# zonghe = Szjilu.query.aggregate(
# zongliushui=Sum('TotalFlow'),
# zongshouyi=Sum('TotalIncome'),
# zongzhichu=Sum('TotalExpense'),
# DailyFlow=Sum('DailyFlow'),
# DailyExpense=Sum('DailyExpense')
# )
# 准备返回数据
if shouzhijilu:
# 有记录的情况
huizongshuju = {
'zongliushui': float(shouzhijilu.TotalFlow or 0),
'zongshouyi': float(shouzhijilu.TotalIncome or 0),
'zongzhichu': float(shouzhijilu.TotalExpense or 0),
'jrls': float(shouzhijilu.DailyFlow or 0),
'jrzc': float(shouzhijilu.DailyExpense or 0)
}
else:
# 没有记录的情况,返回默认值
huizongshuju = {
'zongliushui': 0.00,
'zongshouyi': 0.00,
'zongzhichu': 0.00,
'jrls': 0.00,
'jrzc': 0.00
}
# 6. 查询收支记录表(按俱乐部 xq旧管理员端默认
from jituan.constants import CLUB_ID_DEFAULT
from jituan.services.szjilu_accounting import get_szjilu_snapshot
huizongshuju = get_szjilu_snapshot(CLUB_ID_DEFAULT)
# 7. 返回成功响应
return Response({
@@ -4347,11 +4320,13 @@ class AdcjxgView(APIView):
# 🔥 修改:新购买时只能增加天数
if days > 0:
daoqi_time = timezone.now() + timedelta(days=days)
from jituan.services.club_user import get_user_club_id
new_record = Huiyuangoumai.query.create(
huiyuan_id=huiyuan_id,
yonghu_id=uid,
jieshao=huiyuan.jieshao,
daoqi_time=daoqi_time
daoqi_time=daoqi_time,
club_id=get_user_club_id(User.query.filter(UserUID=uid).first()),
)
# 记录会员购买
@@ -4638,29 +4613,14 @@ class AdtixianqkView(APIView):
# 🟢 新增如果同意提现result=2更新收支记录表
if result == 2:
try:
# 查询收支记录表id=1的记录
szjilu_record = Szjilu.query.get(id=1)
# 获取提现金额
tixian_jine = withdrawal.jine # 假设提现记录模型中有jine字段
update_daily_payout(tixian_jine)
# 更新收支记录
szjilu_record.TotalIncome -= tixian_jine # 总收益减去提现金额
szjilu_record.TotalExpense += tixian_jine # 总支出加上提现金额
szjilu_record.DailyExpense += tixian_jine # 今日支出加上提现金额
szjilu_record.UpdateTime = timezone.now()
szjilu_record.save()
except Szjilu.DoesNotExist:
# 如果收支记录表不存在id=1的记录创建一个
szjilu_record = Szjilu.query.create(
id=1,
TotalIncome=-tixian_jine if result == 2 else 0,
TotalExpense=tixian_jine if result == 2 else 0,
DailyExpense=tixian_jine if result == 2 else 0
from jituan.services.szjilu_accounting import apply_szjilu_expense
tixian_jine = withdrawal.jine
from jituan.services.club_user import get_user_club_id
payout_club = get_user_club_id(
User.query.filter(UserUID=withdrawal.yonghuid).first()
)
update_daily_payout(tixian_jine, payout_club)
apply_szjilu_expense(tixian_jine, payout_club)
except Exception as e:
# 如果更新收支记录失败,抛出异常,事务会回滚
raise Exception(f"更新收支记录失败: {str(e)}")
@@ -5372,6 +5332,22 @@ class WechatLoginAndDashouRegisterView(APIView):
# 这里我们继续执行,但只返回登录信息
return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
from jituan.services.invite_guard import assert_invite_same_club
from jituan.services.club_user import ensure_user_club_id
from jituan.services.club_context import resolve_club_id_from_request
ok, invite_msg = assert_invite_same_club(
request, guanshi_profile.user, user_main,
)
if not ok:
return Response({
'code': 403,
'msg': invite_msg,
'data': None
}, status=status.HTTP_403_FORBIDDEN)
ensure_user_club_id(user_main, resolve_club_id_from_request(request))
# 验证管事状态
# 4.3 检查用户是否已是打手
@@ -7100,8 +7076,12 @@ class KefuGetOrderListView(APIView):
if clkf:
q_conditions &= Q(AssignedCS__icontains=clkf)
from jituan.services.club_context import orders_for_request
# ---------- 统计(仅按发单平台,非跨平台) ----------
stats = Order.query.filter(Platform=1).aggregate(
club_orders = orders_for_request(request)
platform_orders = club_orders.filter(Platform=1)
stats = platform_orders.aggregate(
total_orders=Count('id'),
completed_orders=Count('id', filter=Q(Status=3)),
refund_orders=Count('id', filter=Q(Status=5)),
@@ -7110,20 +7090,20 @@ class KefuGetOrderListView(APIView):
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
status_counts = {
'all': Order.query.filter(Platform=1).count(),
'1,7': Order.query.filter(Platform=1, Status__in=[1,7]).count(),
'2': Order.query.filter(Platform=1, Status=2).count(),
'8': Order.query.filter(Platform=1, Status=8).count(),
'3': Order.query.filter(Platform=1, Status=3).count(),
'4': Order.query.filter(Platform=1, Status=4).count(),
'5': Order.query.filter(Platform=1, Status=5).count(),
'6': Order.query.filter(Platform=1, Status=6).count(),
'all': platform_orders.count(),
'1,7': platform_orders.filter(Status__in=[1,7]).count(),
'2': platform_orders.filter(Status=2).count(),
'8': platform_orders.filter(Status=8).count(),
'3': platform_orders.filter(Status=3).count(),
'4': platform_orders.filter(Status=4).count(),
'5': platform_orders.filter(Status=5).count(),
'6': platform_orders.filter(Status=6).count(),
}
# ---------- 分页查询 ----------
total_count = Order.query.filter(q_conditions).count()
total_count = club_orders.filter(q_conditions).count()
orders_qs = Order.query.filter(q_conditions).select_related(
orders_qs = club_orders.filter(q_conditions).select_related(
'pingtai_kuozhan'
).only(
'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform',
@@ -7158,6 +7138,8 @@ class KefuGetOrderListView(APIView):
'youxi_nicheng': youxi_nicheng
})
from jituan.services.club_user_access import list_response_meta
response_data = {
'stats': {
'totalOrders': stats['total_orders'] or 0,
@@ -7169,7 +7151,8 @@ class KefuGetOrderListView(APIView):
'list': result_list,
'total': total_count,
'page': page,
'pageSize': page_size
'pageSize': page_size,
**list_response_meta(request),
}
logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
@@ -7269,8 +7252,12 @@ class KefuGetShangjiaOrderListView(APIView):
ShopProfile__nicheng__icontains=shangjia_nicheng
).values_list('UserUID', flat=True))
from jituan.services.club_context import orders_for_request
# ---------- 统计(仅按发单平台,非跨平台) ----------
stats = Order.query.filter(Platform=2).aggregate(
club_orders = orders_for_request(request)
merchant_orders = club_orders.filter(Platform=2)
stats = merchant_orders.aggregate(
total_orders=Count('id'),
completed_orders=Count('id', filter=Q(Status=3)),
refund_orders=Count('id', filter=Q(Status=5)),
@@ -7279,20 +7266,20 @@ class KefuGetShangjiaOrderListView(APIView):
# ---------- 状态计数(所有状态都返回,非跨平台) ----------
status_counts = {
'all': Order.query.filter(Platform=2).count(),
'1,7': Order.query.filter(Platform=2, Status__in=[1,7]).count(),
'2': Order.query.filter(Platform=2, Status=2).count(),
'8': Order.query.filter(Platform=2, Status=8).count(),
'3': Order.query.filter(Platform=2, Status=3).count(),
'4': Order.query.filter(Platform=2, Status=4).count(),
'5': Order.query.filter(Platform=2, Status=5).count(),
'6': Order.query.filter(Platform=2, Status=6).count(),
'all': merchant_orders.count(),
'1,7': merchant_orders.filter(Status__in=[1,7]).count(),
'2': merchant_orders.filter(Status=2).count(),
'8': merchant_orders.filter(Status=8).count(),
'3': merchant_orders.filter(Status=3).count(),
'4': merchant_orders.filter(Status=4).count(),
'5': merchant_orders.filter(Status=5).count(),
'6': merchant_orders.filter(Status=6).count(),
}
# ---------- 分页查询 ----------
total_count = Order.query.filter(q_conditions).count()
total_count = club_orders.filter(q_conditions).count()
orders_qs = Order.query.filter(q_conditions).select_related(
orders_qs = club_orders.filter(q_conditions).select_related(
'shangjia_kuozhan'
).only(
'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform',
@@ -7327,6 +7314,8 @@ class KefuGetShangjiaOrderListView(APIView):
'youxi_nicheng': youxi_nicheng
})
from jituan.services.club_user_access import list_response_meta
response_data = {
'stats': {
'totalOrders': stats['total_orders'] or 0,
@@ -7338,7 +7327,8 @@ class KefuGetShangjiaOrderListView(APIView):
'list': result_list,
'total': total_count,
'page': page,
'pageSize': page_size
'pageSize': page_size,
**list_response_meta(request),
}
logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
@@ -7708,7 +7698,7 @@ class KefuPlatformRefundView(APIView):
# 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
#更新支出记录
update_daily_payout(order.Amount)
update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
try:
@@ -9033,14 +9023,18 @@ class KefuPunishmentListView(APIView):
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
# 3. 获取统计信息(不分页)
from jituan.services.club_penalty import filter_penalty_record_qs
from jituan.services.club_user_access import list_response_meta
base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
stats = {
'zongshu': PenaltyRecord.query.count(),
'daichuli': PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count(),
'yichuli': PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count(),
'zongshu': base_qs.count(),
'daichuli': base_qs.filter(ApplyStatus__in=[0, 3]).count(),
'yichuli': base_qs.filter(ApplyStatus__in=[1, 2]).count(),
}
# 4. 构建查询条件
queryset = PenaltyRecord.query.all()
queryset = base_qs
if status_list and isinstance(status_list, list):
queryset = queryset.filter(ApplyStatus__in=status_list)
@@ -9075,7 +9069,8 @@ class KefuPunishmentListView(APIView):
'data': {
'list': list_data,
'total': total,
'stats': stats
'stats': stats,
**list_response_meta(request),
}
})
@@ -9696,12 +9691,14 @@ class KefuWithdrawListView(APIView):
)
return False
def _list_queryset(self, q, shenhe_field_ok):
def _list_queryset(self, q, shenhe_field_ok, request):
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
fields = _TIXIAN_LIST_BASE_FIELDS + (
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
)
return Tixianjilu.query.filter(q).only(*fields)
from jituan.services.tixian_club import filter_tixianjilu_by_request
qs = Tixianjilu.query.filter(q).only(*fields)
return filter_tixianjilu_by_request(qs, request)
def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
"""无关联审核或审核表未标记自动 → 默认手动(1)"""
@@ -9747,7 +9744,7 @@ class KefuWithdrawListView(APIView):
if kefu is None:
return permissions
if '005bb' not in permissions:
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
return Response({'code': 403, 'msg': '您无权查看提现审核列表'})
q = Q()
@@ -9824,11 +9821,11 @@ class KefuWithdrawListView(APIView):
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
base_qs = self._list_queryset(q, shenhe_field_ok)
base_qs = self._list_queryset(q, shenhe_field_ok, request)
total_count = base_qs.count()
awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok).count()
awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok, request).count()
processed_count = self._list_queryset(
q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok,
q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, request,
).count()
agg = base_qs.aggregate(total=Sum('jine'))
total_amount = agg.get('total') or 0
@@ -9888,12 +9885,15 @@ class KefuWithdrawListView(APIView):
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
},
})
@@ -9971,6 +9971,11 @@ class KefuWithdrawDetailView(APIView):
except Tixianjilu.DoesNotExist:
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, record)
if deny:
return deny
# 查询用户主表
try:
user = User.query.get(UserUID=record.yonghuid)
@@ -10113,7 +10118,7 @@ class KefuWithdrawActionView(APIView):
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return [item], None
def _process_auto_item(self, item, action, reason, kefu_user):
def _process_auto_item(self, request, item, action, reason, kefu_user):
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
req_yonghuid = item['yonghuid']
@@ -10127,6 +10132,11 @@ class KefuWithdrawActionView(APIView):
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
if tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if tixian.leixing != req_leixing:
@@ -10181,7 +10191,7 @@ class KefuWithdrawActionView(APIView):
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
def _process_manual_item(self, item, action, reason, phone):
def _process_manual_item(self, request, item, action, reason, phone):
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
try:
@@ -10189,6 +10199,11 @@ class KefuWithdrawActionView(APIView):
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
req_yonghuid = item.get('yonghuid')
req_leixing = item.get('leixing')
if req_yonghuid and tixian.yonghuid != req_yonghuid:
@@ -10206,16 +10221,14 @@ class KefuWithdrawActionView(APIView):
except User.DoesNotExist:
raise ValueError(f'用户{tixian.yonghuid}不存在')
from jituan.services.club_user import get_user_club_id
if action == 1:
tixian.zhuangtai = 2
try:
update_daily_payout(tixian.jine)
szjilu, _ = Szjilu.query.get_or_create(id=1)
szjilu.TotalIncome -= tixian.jine
szjilu.TotalExpense += tixian.jine
szjilu.DailyExpense += tixian.jine
szjilu.UpdateTime = timezone.now()
szjilu.save()
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(tixian.jine, get_user_club_id(user))
apply_szjilu_expense(tixian.jine, get_user_club_id(user))
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
@@ -10265,15 +10278,20 @@ class KefuWithdrawActionView(APIView):
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
if is_auto:
self._process_auto_item(item, action, reason, kefu_user)
self._process_auto_item(request, item, action, reason, kefu_user)
else:
if is_batch:
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
self._process_manual_item(item, action, reason, phone)
self._process_manual_item(request, item, action, reason, phone)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
@@ -10943,6 +10961,12 @@ class GuanshiRegisterView(APIView):
return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
status=status.HTTP_403_FORBIDDEN)
from jituan.services.invite_guard import assert_invite_same_club
ok, invite_msg = assert_invite_same_club(request, zuzhang.user, current_user)
if not ok:
return Response({'code': 403, 'msg': invite_msg, 'data': None},
status=status.HTTP_403_FORBIDDEN)
zuzhang_yonghuid = zuzhang.user.UserUID
# 4. 事务内创建管事实体和打手(如果需要)
@@ -11197,6 +11221,10 @@ class ZuzhangZhuceView(APIView):
if invite_code != self.ACTIVATION_CODE:
return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
from jituan.services.club_user import ensure_user_club_id
from jituan.services.club_context import resolve_club_id_from_request
ensure_user_club_id(user, resolve_club_id_from_request(request))
# 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
yaoqingma = self._generate_unique_yaoqingma(user.UserUID)
@@ -11707,13 +11735,15 @@ class FaKuanPayView(APIView):
random_num = random.randint(0, 99)
dingdanid = f"FK{timestamp_ms:011d}{timestamp_ns:06d}{random_num:02d}"
from jituan.services.club_write import resolve_club_id_for_write
chongzhi_jilu = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9,
yonghuid=request.user.UserUID,
jine=jine,
leixing=4,
shuoming=settings.FAKUAN_PAY_DESCRIPTION
shuoming=settings.FAKUAN_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, request.user),
)
try:
@@ -11746,9 +11776,12 @@ class FaKuanPayView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def generate_wechat_pay_params(self, dingdanid, jine, openid, pay_type):
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
if pay_type == 'fakuan':
PAY_DESCRIPTION = settings.FAKUAN_PAY_DESCRIPTION
@@ -11884,7 +11917,9 @@ class FaKuanHuitiaoView(View):
return self.wechat_response('FAIL', '支付失败')
# 3. 验证签名(确保是微信发来的)
if not self.verify_signature(request_body):
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"签名验证失败: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
@@ -11944,26 +11979,9 @@ class FaKuanHuitiaoView(View):
# 获取订单金额确保是Decimal类型
jine_decimal = Decimal(str(order.jine)) if not isinstance(order.jine, Decimal) else order.jine
update_daily_income(jine_decimal)
# 查询或创建ID=1的收支记录
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00')
}
)
# 更新三个字段
szjilu.TotalIncome += jine_decimal # 总收益
szjilu.TotalFlow += jine_decimal # 总流水
szjilu.DailyFlow += jine_decimal # 今日流水
szjilu.save()
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"收支记录更新成功: 订单{order.dingdan_id}, 金额{jine_decimal}")
@@ -12181,13 +12199,15 @@ class KaohePayView(APIView):
logger.info(f"生成订单ID: {dingdanid}")
# 8. 创建支付订单Czjilu
from jituan.services.club_write import resolve_club_id_for_write
order = Czjilu.query.create(
dingdan_id=dingdanid,
zhuangtai=9, # 未支付
yonghuid=user.UserUID,
jine=float(fee),
leixing=5, # 5-考核支付
shuoming=settings.KAOHE_PAY_DESCRIPTION
shuoming=settings.KAOHE_PAY_DESCRIPTION,
club_id=resolve_club_id_for_write(request, user),
)
logger.info(f"创建支付订单成功: {dingdanid}")
@@ -12249,9 +12269,12 @@ class KaohePayView(APIView):
def generate_wechat_pay_params(self, dingdanid, jine, openid, attach, pay_type):
"""生成微信JSAPI支付参数"""
APPID = settings.WEIXIN_APPID
MCHID = settings.WEIXIN_MCHID
KEY = settings.WEIXIN_SHANGHUMIYAO
from jituan.services.club_write import resolve_club_id_for_write
from jituan.services.wechat_pay import get_wechat_v2_config
pay_cfg = get_wechat_v2_config(resolve_club_id_for_write(self.request, request.user))
APPID = pay_cfg['appid']
MCHID = pay_cfg['mch_id']
KEY = pay_cfg['key']
if pay_type == 'kaohe':
PAY_DESCRIPTION = settings.KAOHE_PAY_DESCRIPTION
@@ -12441,7 +12464,9 @@ class KaohePayCallbackView(View):
return self.wechat_response('FAIL', '支付失败')
# 验证签名
if not self.verify_signature(request_body):
from jituan.services.wechat_pay import club_id_from_czjilu, verify_wechat_v2_signature
notify_club = club_id_from_czjilu(out_trade_no)
if not verify_wechat_v2_signature(request_body, notify_club):
logger.error(f"签名验证失败,订单号: {out_trade_no}")
return self.wechat_response('FAIL', '签名验证失败')
logger.info(f"签名验证成功,订单号: {out_trade_no}")
@@ -12513,24 +12538,11 @@ class KaohePayCallbackView(View):
# 更新收支记录
try:
jine_decimal = Decimal(str(order.jine))
update_daily_income(jine_decimal)
update_daily_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"更新每日收入成功: {jine_decimal}")
szjilu, created = Szjilu.query.get_or_create(
id=1,
defaults={
'TotalIncome': Decimal('0.00'),
'TotalFlow': Decimal('0.00'),
'TotalExpense': Decimal('0.00'),
'DailyExpense': Decimal('0.00'),
'DailyFlow': Decimal('0.00'),
}
)
szjilu.TotalIncome += jine_decimal
szjilu.TotalFlow += jine_decimal
szjilu.DailyFlow += jine_decimal
szjilu.save()
logger.info(f"更新收支记录成功: zongsy={szjilu.TotalIncome}")
from jituan.services.szjilu_accounting import apply_szjilu_income
apply_szjilu_income(jine_decimal, getattr(order, 'club_id', None))
logger.info(f"更新收支记录成功")
except Exception as e:
logger.error(f"更新收支记录失败: {str(e)}", exc_info=True)
@@ -12709,30 +12721,11 @@ class TixianAssetView(APIView):
except ObjectDoesNotExist:
pass
# ------------------- 费率查询(从 CommissionRate 获取) -------------------
# 打手提现费率 (类型 '5')
lilu_obj = CommissionRate.query.filter(Platform='5').first()
data['dashou_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import build_tixian_asset_rates
# 打手保证金提现费率 (类型 '11')
lilu_obj = CommissionRate.query.filter(Platform='11').first()
data['dashou_yajin_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
# 商家提现费率 (类型 '10')
lilu_obj = CommissionRate.query.filter(Platform='10').first()
data['shangjia_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
# 管事提现费率 (类型 '6')
lilu_obj = CommissionRate.query.filter(Platform='6').first()
data['guanshi_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
# 组长提现费率 (类型 '8')
lilu_obj = CommissionRate.query.filter(Platform='8').first()
data['zuzhang_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
# 审核官(考核官)提现费率 (类型 '9')
lilu_obj = CommissionRate.query.filter(Platform='9').first()
data['kaoheguan_rate'] = float(lilu_obj.Rate) if lilu_obj and lilu_obj.Rate is not None else 0.00
club_id = get_user_club_id(user)
data.update(build_tixian_asset_rates(club_id))
return Response({
'code': 200,
@@ -12861,8 +12854,10 @@ class TixianCallbackV3View(APIView):
headers = request.headers
body = request.body.decode('utf-8')
# 1. 验证签名
if not verify_wechat_sign(headers, body):
# 1. 验证签名(多商户依次验签)
from utils.wechat_v3 import resolve_callback_club_id, decrypt_callback_ciphertext
callback_club_id = resolve_callback_club_id(headers, body)
if not callback_club_id:
return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 解密数据
@@ -12878,7 +12873,7 @@ class TixianCallbackV3View(APIView):
if not nonce or not ciphertext:
return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext)
plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=callback_club_id)
callback_data = json.loads(plaintext)
except Exception as e:
logger.error(f"回调解密失败: {str(e)}", exc_info=True)

View File

@@ -3,6 +3,7 @@ import time
import hashlib
import base64
import json
import os
from urllib.parse import urlparse
from cryptography.hazmat.primitives import serialization, hashes
@@ -11,29 +12,33 @@ from cryptography.hazmat.backends import default_backend
from Crypto.Cipher import AES
from django.conf import settings
import os
def load_private_key():
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
key_path = settings.WECHAT_PAY_V3_CONFIG['PRIVATE_KEY_PATH']
def _get_v3_config(club_id=None):
from jituan.services.wechat_pay import get_wechat_v3_config
return get_wechat_v3_config(club_id)
def load_private_key(club_id=None):
"""加载私钥(支持 PKCS#1 和 PKCS#8"""
cfg = _get_v3_config(club_id)
key_path = cfg['PRIVATE_KEY_PATH']
with open(key_path, 'rb') as f:
key_data = f.read()
# cryptography 自动识别格式
private_key = serialization.load_pem_private_key(
return serialization.load_pem_private_key(
key_data,
password=None,
backend=default_backend()
backend=default_backend(),
)
return private_key
def build_authorization(method, url, body):
"""生成V3接口的Authorization"""
private_key = load_private_key()
mchid = settings.WECHAT_PAY_V3_CONFIG['MCHID']
serial_no = settings.WECHAT_PAY_V3_CONFIG['CERT_SERIAL_NO']
def build_authorization(method, url, body, club_id=None):
"""生成 V3 接口的 Authorization 头。"""
cfg = _get_v3_config(club_id)
private_key = load_private_key(club_id)
mchid = cfg['MCHID']
serial_no = cfg['CERT_SERIAL_NO']
parsed = urlparse(url)
path = parsed.path
@@ -43,25 +48,25 @@ def build_authorization(method, url, body):
timestamp = str(int(time.time()))
nonce = hashlib.md5((timestamp + 'wechat').encode()).hexdigest()[:32]
# 构造签名串
message = '\n'.join([method, path, timestamp, nonce, body]) + '\n'
# 使用 cryptography 签名
signature = private_key.sign(
message.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
hashes.SHA256(),
)
signature_b64 = base64.b64encode(signature).decode('utf-8')
auth = f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
return auth
return (
f'WECHATPAY2-SHA256-RSA2048 mchid="{mchid}",nonce_str="{nonce}",'
f'timestamp="{timestamp}",serial_no="{serial_no}",signature="{signature_b64}"'
)
def verify_wechat_sign(headers, body):
"""验证微信回调签名(需提前下载平台证书)"""
# 这部分使用 rsa 库验签,也可以改用 cryptography但 rsa 已足够
def verify_wechat_sign(headers, body, club_id=None):
"""验证微信回调签名(需提前下载平台证书)"""
import rsa
serial = headers.get('Wechatpay-Serial')
signature = headers.get('Wechatpay-Signature')
timestamp = headers.get('Wechatpay-Timestamp')
@@ -75,13 +80,14 @@ def verify_wechat_sign(headers, body):
try:
ts = int(headers.get('Wechatpay-Timestamp', '0'))
if abs(time.time() - ts) > 300: # 5分钟有效期
if abs(time.time() - ts) > 300:
return False
except (ValueError, TypeError):
return False
cfg = _get_v3_config(club_id)
message = '\n'.join([timestamp, nonce, body]) + '\n'
cert_path = f"{settings.WECHAT_PAY_V3_CONFIG['PLATFORM_CERT_DIR']}/{serial}.pem"
cert_path = f"{cfg['PLATFORM_CERT_DIR']}/{serial}.pem"
if not os.path.exists(cert_path):
return False
@@ -96,9 +102,27 @@ def verify_wechat_sign(headers, body):
return False
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext):
"""解密回调数据(使用 AES-GCM"""
api_v3_key = settings.WECHAT_PAY_V3_CONFIG['API_V3_KEY'].encode('utf-8')
def resolve_callback_club_id(headers, body):
"""
多商户回调:依次用各俱乐部平台证书验签,返回匹配的 club_id。
验签失败返回 None。
"""
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import Club
if verify_wechat_sign(headers, body, club_id=CLUB_ID_DEFAULT):
return CLUB_ID_DEFAULT
for club in Club.query.filter(status=1).exclude(club_id=CLUB_ID_DEFAULT):
if verify_wechat_sign(headers, body, club_id=club.club_id):
return club.club_id
return None
def decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=None):
"""解密回调数据(使用 AES-GCM"""
cfg = _get_v3_config(club_id)
api_v3_key = cfg['API_V3_KEY'].encode('utf-8')
nonce_bytes = nonce.encode('utf-8')
ciphertext_bytes = base64.b64decode(ciphertext)