feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置
This commit is contained in:
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': '设置修改成功'})
|
||||
Reference in New Issue
Block a user