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

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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