feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置
This commit is contained in:
@@ -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 访问控制 ====================
|
||||
|
||||
@@ -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')),
|
||||
]
|
||||
|
||||
# 重要说明:
|
||||
|
||||
28
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal file
28
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
56
backend/migrations/0004_role_daily_club_id.py
Normal file
56
backend/migrations/0004_role_daily_club_id.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
@@ -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}"
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
473
backend/view.py
473
backend/view.py
@@ -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. 手续费利率(CommissionRate:5打手/6管事/8组长/9审核官/10商家/11打手押金)
|
||||
rate_map = {}
|
||||
for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
|
||||
obj = CommissionRate.query.filter(Platform=code).first()
|
||||
rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0
|
||||
|
||||
# 1b. 订单/押金分红费率(CommissionRate:1平台订单/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.leixing(4~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': [
|
||||
{
|
||||
|
||||
47
config/migrations/0004_daily_stat_club_id.py
Normal file
47
config/migrations/0004_daily_stat_club_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
37
config/migrations/0005_szjilu_club_id.py
Normal file
37
config/migrations/0005_szjilu_club_id.py
Normal 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',
|
||||
),
|
||||
),
|
||||
]
|
||||
60
config/migrations/0006_display_club_id.py
Normal file
60
config/migrations/0006_display_club_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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})'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
0
jituan/__init__.py
Normal file
7
jituan/apps.py
Normal file
7
jituan/apps.py
Normal 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
27
jituan/constants.py
Normal 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'
|
||||
0
jituan/management/__init__.py
Normal file
0
jituan/management/__init__.py
Normal file
0
jituan/management/commands/__init__.py
Normal file
0
jituan/management/commands/__init__.py
Normal file
47
jituan/management/commands/backfill_display_club_id.py
Normal file
47
jituan/management/commands/backfill_display_club_id.py
Normal 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"]}'
|
||||
))
|
||||
23
jituan/management/commands/backfill_gsfenhong_club_id.py
Normal file
23
jituan/management/commands/backfill_gsfenhong_club_id.py
Normal 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} 条'))
|
||||
42
jituan/management/commands/backfill_penalty_club_id.py
Normal file
42
jituan/management/commands/backfill_penalty_club_id.py
Normal 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} 条'))
|
||||
@@ -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}'
|
||||
))
|
||||
36
jituan/management/commands/backfill_role_daily_club_id.py
Normal file
36
jituan/management/commands/backfill_role_daily_club_id.py
Normal 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} 条'))
|
||||
42
jituan/management/commands/backfill_tixian_club_id.py
Normal file
42
jituan/management/commands/backfill_tixian_club_id.py
Normal 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"]}'
|
||||
))
|
||||
53
jituan/management/commands/backfill_user_club_id.py
Normal file
53
jituan/management/commands/backfill_user_club_id.py
Normal 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.ClubID:OpenID 绑定 → 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}'))
|
||||
104
jituan/management/commands/create_club.py
Normal file
104
jituan/management/commands/create_club.py
Normal 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} 复制'))
|
||||
65
jituan/management/commands/seed_admin_assignments.py
Normal file
65
jituan/management/commands/seed_admin_assignments.py
Normal 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})'
|
||||
))
|
||||
183
jituan/management/commands/seed_club_xq.py
Normal file
183
jituan/management/commands/seed_club_xq.py
Normal 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
|
||||
40
jituan/management/commands/seed_szjilu_clubs.py
Normal file
40
jituan/management/commands/seed_szjilu_clubs.py
Normal 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
20
jituan/middleware.py
Normal 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)
|
||||
183
jituan/migrations/0001_initial.py
Normal file
183
jituan/migrations/0001_initial.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
0
jituan/migrations/__init__.py
Normal file
0
jituan/migrations/__init__.py
Normal file
157
jituan/models.py
Normal file
157
jituan/models.py
Normal 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']]
|
||||
0
jituan/services/__init__.py
Normal file
0
jituan/services/__init__.py
Normal file
114
jituan/services/admin_audit.py
Normal file
114
jituan/services/admin_audit.py
Normal 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)
|
||||
102
jituan/services/admin_context.py
Normal file
102
jituan/services/admin_context.py
Normal 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,
|
||||
}
|
||||
216
jituan/services/caiwu_stats.py
Normal file
216
jituan/services/caiwu_stats.py
Normal 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,
|
||||
}
|
||||
53
jituan/services/club_config.py
Normal file
53
jituan/services/club_config.py
Normal 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)})()
|
||||
87
jituan/services/club_context.py
Normal file
87
jituan/services/club_context.py
Normal 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))
|
||||
102
jituan/services/club_penalty.py
Normal file
102
jituan/services/club_penalty.py
Normal 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))
|
||||
79
jituan/services/club_resolver.py
Normal file
79
jituan/services/club_resolver.py
Normal 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)}'}
|
||||
34
jituan/services/club_user.py
Normal file
34
jituan/services/club_user.py
Normal 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'])
|
||||
27
jituan/services/club_user_access.py
Normal file
27
jituan/services/club_user_access.py
Normal 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),
|
||||
}
|
||||
18
jituan/services/club_write.py
Normal file
18
jituan/services/club_write.py
Normal 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
|
||||
129
jituan/services/display_config.py
Normal file
129
jituan/services/display_config.py
Normal 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,
|
||||
)
|
||||
492
jituan/services/finance_detail_stats.py
Normal file
492
jituan/services/finance_detail_stats.py
Normal 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
|
||||
24
jituan/services/invite_guard.py
Normal file
24
jituan/services/invite_guard.py
Normal 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, ''
|
||||
24
jituan/services/shenhe_club.py
Normal file
24
jituan/services/shenhe_club.py
Normal 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))
|
||||
84
jituan/services/szjilu_accounting.py
Normal file
84
jituan/services/szjilu_accounting.py
Normal 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
|
||||
119
jituan/services/szxx_stats.py
Normal file
119
jituan/services/szxx_stats.py
Normal 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
|
||||
32
jituan/services/tixian_club.py
Normal file
32
jituan/services/tixian_club.py
Normal 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
|
||||
80
jituan/services/user_stats.py
Normal file
80
jituan/services/user_stats.py
Normal 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
|
||||
209
jituan/services/wechat_login.py
Normal file
209
jituan/services/wechat_login.py
Normal 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
|
||||
160
jituan/services/wechat_pay.py
Normal file
160
jituan/services/wechat_pay.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""各俱乐部微信支付 V2(MD5)配置与验签。"""
|
||||
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)
|
||||
267
jituan/services/withdraw_config.py
Normal file
267
jituan/services/withdraw_config.py
Normal 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 → 平台日总限额配置 leixing(club_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': '设置修改成功'})
|
||||
130
jituan/sql/phase1_add_club_id.sql
Normal file
130
jituan/sql/phase1_add_club_id.sql
Normal 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
70
jituan/urls.py
Normal 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
477
jituan/views.py
Normal 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
157
jituan/views_display.py
Normal 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': '保存成功'})
|
||||
24
orders/migrations/0004_order_clubid.py
Normal file
24
orders/migrations/0004_order_clubid.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
22
orders/migrations/0005_penalty_clubid.py
Normal file
22
orders/migrations/0005_penalty_clubid.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
22
orders/migrations/0006_penaltybonus_clubid.py
Normal file
22
orders/migrations/0006_penaltybonus_clubid.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
22
orders/migrations/0007_penaltyrecord_clubid.py
Normal file
22
orders/migrations/0007_penaltyrecord_clubid.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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='创建时间',
|
||||
|
||||
@@ -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}, "
|
||||
|
||||
112
orders/views.py
112
orders/views.py
@@ -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': '罚款申请已提交'})
|
||||
|
||||
25
products/migrations/0003_czjilu_huiyuangoumai_club_id.py
Normal file
25
products/migrations/0003_czjilu_huiyuangoumai_club_id.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
18
products/migrations/0004_gsfenhong_club_id.py
Normal file
18
products/migrations/0004_gsfenhong_club_id.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
20
products/migrations/0005_duoci_fenhong_club_id.py
Normal file
20
products/migrations/0005_duoci_fenhong_club_id.py
Normal 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')},
|
||||
),
|
||||
]
|
||||
@@ -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']),
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
20
users/migrations/0002_user_clubid.py
Normal file
20
users/migrations/0002_user_clubid.py
Normal 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='所属俱乐部',
|
||||
),
|
||||
),
|
||||
]
|
||||
30
users/migrations/0003_tixian_club_id.py
Normal file
30
users/migrations/0003_tixian_club_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
@@ -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='申请提现金额')
|
||||
|
||||
@@ -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打手→配置4,2管事→5,3组长→6,4考核官→7,5押金→8,6商家→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 '',
|
||||
|
||||
@@ -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')
|
||||
|
||||
395
users/views.py
395
users/views.py
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user