feat: 会员体验版、俱乐部分离上架与分红计数加固
扩展 club_huiyuan_price/huiyuangoumai/czjilu/gsfenhong 字段;微信购会员强制写 czjilu+收支入账;formal_cishu 仅统计正式已支付单;关闭余额购会员;俱乐部上架/下架 API。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
122
backend/view.py
122
backend/view.py
@@ -1276,28 +1276,20 @@ class KefuUpdateDashouView(APIView):
|
|||||||
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
||||||
if not huiyuan_id:
|
if not huiyuan_id:
|
||||||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||||
days = int(value) if value else 30
|
|
||||||
try:
|
try:
|
||||||
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||||
except Huiyuan.DoesNotExist:
|
except Huiyuan.DoesNotExist:
|
||||||
return Response({'code': 404, 'msg': '会员类型不存在'})
|
return Response({'code': 404, 'msg': '会员类型不存在'})
|
||||||
from jituan.services.club_user import get_user_club_id
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
|
||||||
player_club = get_user_club_id(dashou_user)
|
player_club = get_user_club_id(dashou_user)
|
||||||
record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
|
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
|
||||||
yonghu_id=dashou_id,
|
days = int(value) if value else int(row.formal_days or 30) if row else 30
|
||||||
huiyuan_id=huiyuan_id,
|
record, err = extend_member_entitlement_admin(
|
||||||
defaults={
|
dashou_id, huiyuan_id, days, player_club,
|
||||||
'huiyuan_zhuangtai': 1,
|
|
||||||
'jieshao': huiyuan.jieshao,
|
|
||||||
'daoqi_time': timezone.now() + timedelta(days=days),
|
|
||||||
'club_id': player_club,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if not created:
|
if err:
|
||||||
new_daoqi = record.daoqi_time + timedelta(days=days)
|
return Response({'code': 400, 'msg': err})
|
||||||
record.daoqi_time = new_daoqi
|
|
||||||
record.huiyuan_zhuangtai = 1
|
|
||||||
record.save()
|
|
||||||
write_xiugai_log(
|
write_xiugai_log(
|
||||||
yonghuid=dashou_id,
|
yonghuid=dashou_id,
|
||||||
xiugaiid=kefu.user.Phone,
|
xiugaiid=kefu.user.Phone,
|
||||||
@@ -1305,9 +1297,9 @@ class KefuUpdateDashouView(APIView):
|
|||||||
huiyuants=days,
|
huiyuants=days,
|
||||||
huiyuan_id=huiyuan_id,
|
huiyuan_id=huiyuan_id,
|
||||||
qitashuoming=(
|
qitashuoming=(
|
||||||
f'【后台】给打手添加会员:会员ID={huiyuan_id}({huiyuan.jieshao}),'
|
f'【后台】给打手添加正式会员:会员ID={huiyuan_id}({huiyuan.jieshao}),'
|
||||||
f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},'
|
f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},'
|
||||||
f'打手ID={dashou_id},操作人={kefu.user.Phone}'
|
f'打手ID={dashou_id},操作人={kefu.user.Phone}(无支付单、无分红)'
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
||||||
@@ -2827,8 +2819,16 @@ class UpdateMemberView(APIView):
|
|||||||
|
|
||||||
from jituan.services.club_member_admin import update_member_for_club
|
from jituan.services.club_member_admin import update_member_for_club
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
'formal_days': request.data.get('formal_days'),
|
||||||
|
'trial_enabled': request.data.get('trial_enabled'),
|
||||||
|
'trial_price': request.data.get('trial_price'),
|
||||||
|
'trial_days': request.data.get('trial_days'),
|
||||||
|
'trial_guanshifc': request.data.get('trial_guanshifc'),
|
||||||
|
'trial_zuzhangfc': request.data.get('trial_zuzhangfc'),
|
||||||
|
}
|
||||||
data, err = update_member_for_club(
|
data, err = update_member_for_club(
|
||||||
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao,
|
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra,
|
||||||
)
|
)
|
||||||
if err:
|
if err:
|
||||||
return Response({'code': 400, 'msg': err})
|
return Response({'code': 400, 'msg': err})
|
||||||
@@ -2880,11 +2880,17 @@ class AddMemberView(APIView):
|
|||||||
if '3300a' not in permissions:
|
if '3300a' not in permissions:
|
||||||
return Response({'code': 403, 'msg': '无权限添加会员'})
|
return Response({'code': 403, 'msg': '无权限添加会员'})
|
||||||
|
|
||||||
from jituan.services.club_member_admin import add_member_for_club
|
from jituan.services.club_member_admin import add_global_member
|
||||||
|
from jituan.services.club_context import resolve_club_scope
|
||||||
|
from jituan.constants import DATA_SCOPE_ALL
|
||||||
|
|
||||||
data, err = add_member_for_club(
|
if resolve_club_scope(request) != DATA_SCOPE_ALL and '000001' not in permissions:
|
||||||
request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao,
|
return Response({
|
||||||
)
|
'code': 403,
|
||||||
|
'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架',
|
||||||
|
})
|
||||||
|
|
||||||
|
data, err = add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao)
|
||||||
if err:
|
if err:
|
||||||
return Response({'code': 400, 'msg': err})
|
return Response({'code': 400, 'msg': err})
|
||||||
return Response({'code': 0, 'msg': '添加成功', 'data': data})
|
return Response({'code': 0, 'msg': '添加成功', 'data': data})
|
||||||
@@ -2892,6 +2898,78 @@ class AddMemberView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberCatalogView(APIView):
|
||||||
|
"""俱乐部可上架的集团会员 catalog。POST /houtai/hthycatalog"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
from jituan.services.club_member_admin import build_member_catalog_payload
|
||||||
|
return Response({'code': 0, 'data': build_member_catalog_payload(request)})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberEnableView(APIView):
|
||||||
|
"""俱乐部上架会员。POST /houtai/hthysj"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||||
|
if not username or not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
try:
|
||||||
|
jiage = Decimal(str(request.data.get('jiage')))
|
||||||
|
guanshifc = Decimal(str(request.data.get('guanshifc')))
|
||||||
|
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
|
||||||
|
except Exception:
|
||||||
|
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||||
|
from jituan.services.club_member_admin import enable_member_for_club
|
||||||
|
extra = request.data
|
||||||
|
data, err = enable_member_for_club(
|
||||||
|
request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=extra,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '上架成功', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberDisableView(APIView):
|
||||||
|
"""俱乐部下架会员。POST /houtai/hthyxj"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||||
|
if not username or not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
from jituan.services.club_member_admin import disable_member_for_club
|
||||||
|
data, err = disable_member_for_club(request, huiyuan_id)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '已下架', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GetGuanliListView(APIView):
|
class GetGuanliListView(APIView):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ class Command(BaseCommand):
|
|||||||
jiage=row.jiage or Decimal('0'),
|
jiage=row.jiage or Decimal('0'),
|
||||||
guanshifc=row.guanshifc or Decimal('0'),
|
guanshifc=row.guanshifc or Decimal('0'),
|
||||||
zuzhangfc=row.zuzhangfc or Decimal('0'),
|
zuzhangfc=row.zuzhangfc or Decimal('0'),
|
||||||
|
formal_days=row.formal_days or 30,
|
||||||
|
trial_enabled=bool(row.trial_enabled),
|
||||||
|
trial_price=row.trial_price or Decimal('0'),
|
||||||
|
trial_days=row.trial_days or 0,
|
||||||
|
trial_guanshifc=row.trial_guanshifc or Decimal('0'),
|
||||||
|
trial_zuzhangfc=row.trial_zuzhangfc or Decimal('0'),
|
||||||
is_enabled=row.is_enabled,
|
is_enabled=row.is_enabled,
|
||||||
bankuai_id=row.bankuai_id,
|
bankuai_id=row.bankuai_id,
|
||||||
)
|
)
|
||||||
|
|||||||
48
jituan/migrations/0006_club_huiyuan_trial_fields.py
Normal file
48
jituan/migrations/0006_club_huiyuan_trial_fields.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0005_identity_tag'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='formal_days',
|
||||||
|
field=models.PositiveIntegerField(default=30, verbose_name='正式会员有效天数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='trial_enabled',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否开启体验会员'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='trial_price',
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='体验会员价格',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='trial_days',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='体验会员有效天数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='trial_guanshifc',
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='体验管事分成',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubhuiyuanprice',
|
||||||
|
name='trial_zuzhangfc',
|
||||||
|
field=models.DecimalField(
|
||||||
|
decimal_places=2, default=Decimal('0.00'), max_digits=10, verbose_name='体验组长分成',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -99,6 +99,18 @@ class ClubHuiyuanPrice(QModel):
|
|||||||
jiage = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
jiage = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
guanshifc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
guanshifc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
zuzhangfc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
zuzhangfc = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||||
|
formal_days = models.PositiveIntegerField(default=30, verbose_name='正式会员有效天数')
|
||||||
|
trial_enabled = models.BooleanField(default=False, verbose_name='是否开启体验会员')
|
||||||
|
trial_price = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2, default=Decimal('0.00'), verbose_name='体验会员价格',
|
||||||
|
)
|
||||||
|
trial_days = models.PositiveIntegerField(default=0, verbose_name='体验会员有效天数')
|
||||||
|
trial_guanshifc = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2, default=Decimal('0.00'), verbose_name='体验管事分成',
|
||||||
|
)
|
||||||
|
trial_zuzhangfc = models.DecimalField(
|
||||||
|
max_digits=10, decimal_places=2, default=Decimal('0.00'), verbose_name='体验组长分成',
|
||||||
|
)
|
||||||
is_enabled = models.BooleanField(default=True)
|
is_enabled = models.BooleanField(default=True)
|
||||||
bankuai_id = models.IntegerField(null=True, blank=True)
|
bankuai_id = models.IntegerField(null=True, blank=True)
|
||||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""后台会员管理:全局 Huiyuan 定义 + 各俱乐部 ClubHuiyuanPrice 售价/分成。"""
|
"""后台会员管理:全局 Huiyuan 定义 + 各俱乐部 ClubHuiyuanPrice 售价/分成/体验配置。"""
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
@@ -14,13 +14,26 @@ from products.models import Huiyuan
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CLUB_WRITE_SCOPE_MSG = '当前为集团汇总视图,请先在顶栏切换到具体俱乐部(如星阙、星之界)后再操作'
|
CLUB_WRITE_SCOPE_MSG = '当前为集团汇总视图,请先在顶栏切换到具体俱乐部后再操作'
|
||||||
|
|
||||||
|
|
||||||
def club_write_blocked(request):
|
def club_write_blocked(request):
|
||||||
return resolve_club_scope(request) == DATA_SCOPE_ALL
|
return resolve_club_scope(request) == DATA_SCOPE_ALL
|
||||||
|
|
||||||
|
|
||||||
|
def _price_row_to_dict(row):
|
||||||
|
if not row:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
'formal_days': int(row.formal_days or 30),
|
||||||
|
'trial_enabled': bool(row.trial_enabled),
|
||||||
|
'trial_price': str(row.trial_price or 0),
|
||||||
|
'trial_days': int(row.trial_days or 0),
|
||||||
|
'trial_guanshifc': str(row.trial_guanshifc or 0),
|
||||||
|
'trial_zuzhangfc': str(row.trial_zuzhangfc or 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _format_member(m, price_row=None):
|
def _format_member(m, price_row=None):
|
||||||
global_jiage = str(m.jiage)
|
global_jiage = str(m.jiage)
|
||||||
global_guanshifc = str(m.guanshifc)
|
global_guanshifc = str(m.guanshifc)
|
||||||
@@ -41,6 +54,12 @@ def _format_member(m, price_row=None):
|
|||||||
'bankuai_id': m.bankuai_id,
|
'bankuai_id': m.bankuai_id,
|
||||||
'club_enabled': True,
|
'club_enabled': True,
|
||||||
'price_source': 'global',
|
'price_source': 'global',
|
||||||
|
'formal_days': 30,
|
||||||
|
'trial_enabled': False,
|
||||||
|
'trial_price': '0.00',
|
||||||
|
'trial_days': 0,
|
||||||
|
'trial_guanshifc': '0.00',
|
||||||
|
'trial_zuzhangfc': '0.00',
|
||||||
}
|
}
|
||||||
if price_row is not None:
|
if price_row is not None:
|
||||||
item['jiage'] = str(price_row.jiage)
|
item['jiage'] = str(price_row.jiage)
|
||||||
@@ -48,6 +67,7 @@ def _format_member(m, price_row=None):
|
|||||||
item['zuzhangfc'] = str(price_row.zuzhangfc)
|
item['zuzhangfc'] = str(price_row.zuzhangfc)
|
||||||
item['club_enabled'] = bool(price_row.is_enabled)
|
item['club_enabled'] = bool(price_row.is_enabled)
|
||||||
item['price_source'] = 'club'
|
item['price_source'] = 'club'
|
||||||
|
item.update(_price_row_to_dict(price_row))
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -81,20 +101,70 @@ def build_member_list_payload(request):
|
|||||||
'club_id': club_id,
|
'club_id': club_id,
|
||||||
'scope': scope,
|
'scope': scope,
|
||||||
'price_note': (
|
'price_note': (
|
||||||
f'当前俱乐部 {club_id} 专属售价(表 club_huiyuan_price)。'
|
f'当前俱乐部 {club_id} 专属售价(club_huiyuan_price)。'
|
||||||
'若与星阙相同,是创建时从 xq 复制的默认值,请在本俱乐部视图下修改价格。'
|
|
||||||
if scope != DATA_SCOPE_ALL else
|
if scope != DATA_SCOPE_ALL else
|
||||||
'集团汇总:展示全局默认价。请切换到具体俱乐部查看/编辑专属价。'
|
'集团汇总:展示全局默认价;切换具体俱乐部可配置售价与体验会员。'
|
||||||
),
|
),
|
||||||
**list_response_meta(request),
|
**list_response_meta(request),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao):
|
def build_member_catalog_payload(request):
|
||||||
|
"""集团会员类型 + 本俱乐部未上架的可选列表。"""
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
enabled_ids = set(
|
||||||
|
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
|
||||||
|
)
|
||||||
|
all_members = Huiyuan.query.all().order_by('-CreateTime')
|
||||||
|
catalog = []
|
||||||
|
for m in all_members:
|
||||||
|
if m.huiyuan_id in enabled_ids:
|
||||||
|
continue
|
||||||
|
catalog.append({
|
||||||
|
'huiyuan_id': m.huiyuan_id,
|
||||||
|
'jieshao': m.jieshao,
|
||||||
|
'jtjieshao': m.jtjieshao,
|
||||||
|
})
|
||||||
|
return {'club_id': club_id, 'catalog': catalog}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_trial_fields(data, jiage, guanshifc, zuzhangfc):
|
||||||
|
formal_days = int(data.get('formal_days') or 30)
|
||||||
|
trial_enabled = bool(data.get('trial_enabled'))
|
||||||
|
trial_price = Decimal(str(data.get('trial_price') or 0))
|
||||||
|
trial_days = int(data.get('trial_days') or 0)
|
||||||
|
trial_guanshifc = Decimal(str(data.get('trial_guanshifc') or 0))
|
||||||
|
trial_zuzhangfc = Decimal(str(data.get('trial_zuzhangfc') or 0))
|
||||||
|
if formal_days < 1 or formal_days > 3650:
|
||||||
|
raise ValueError('正式会员天数无效')
|
||||||
|
if trial_enabled:
|
||||||
|
if trial_price <= 0 or trial_days <= 0:
|
||||||
|
raise ValueError('开启体验会员须配置体验价格与天数')
|
||||||
|
if trial_guanshifc + trial_zuzhangfc > trial_price:
|
||||||
|
raise ValueError('体验管事+组长分成不能大于体验价格')
|
||||||
|
if guanshifc + zuzhangfc > jiage:
|
||||||
|
raise ValueError('管事分成+组长分成不能大于会员价格')
|
||||||
|
return {
|
||||||
|
'formal_days': formal_days,
|
||||||
|
'trial_enabled': trial_enabled,
|
||||||
|
'trial_price': trial_price,
|
||||||
|
'trial_days': trial_days,
|
||||||
|
'trial_guanshifc': trial_guanshifc,
|
||||||
|
'trial_zuzhangfc': trial_zuzhangfc,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=None):
|
||||||
if club_write_blocked(request):
|
if club_write_blocked(request):
|
||||||
return None, CLUB_WRITE_SCOPE_MSG
|
return None, CLUB_WRITE_SCOPE_MSG
|
||||||
|
|
||||||
club_id = club_id_for_write(request)
|
club_id = club_id_for_write(request)
|
||||||
|
extra = extra or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
trial_fields = _parse_trial_fields(extra, jiage, guanshifc, zuzhangfc)
|
||||||
|
except ValueError as e:
|
||||||
|
return None, str(e)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
try:
|
try:
|
||||||
@@ -115,6 +185,7 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha
|
|||||||
'zuzhangfc': zuzhangfc,
|
'zuzhangfc': zuzhangfc,
|
||||||
'is_enabled': True,
|
'is_enabled': True,
|
||||||
'bankuai_id': member.bankuai_id,
|
'bankuai_id': member.bankuai_id,
|
||||||
|
**trial_fields,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if not created:
|
if not created:
|
||||||
@@ -122,6 +193,8 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha
|
|||||||
row.guanshifc = guanshifc
|
row.guanshifc = guanshifc
|
||||||
row.zuzhangfc = zuzhangfc
|
row.zuzhangfc = zuzhangfc
|
||||||
row.is_enabled = True
|
row.is_enabled = True
|
||||||
|
for k, v in trial_fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
row.save()
|
row.save()
|
||||||
|
|
||||||
if club_id == CLUB_ID_DEFAULT:
|
if club_id == CLUB_ID_DEFAULT:
|
||||||
@@ -133,11 +206,66 @@ def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzha
|
|||||||
return {'huiyuan_id': huiyuan_id}, None
|
return {'huiyuan_id': huiyuan_id}, None
|
||||||
|
|
||||||
|
|
||||||
def add_member_for_club(request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None):
|
def enable_member_for_club(request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=None):
|
||||||
|
"""俱乐部上架集团会员类型。"""
|
||||||
if club_write_blocked(request):
|
if club_write_blocked(request):
|
||||||
return None, CLUB_WRITE_SCOPE_MSG
|
return None, CLUB_WRITE_SCOPE_MSG
|
||||||
|
|
||||||
club_id = club_id_for_write(request)
|
club_id = club_id_for_write(request)
|
||||||
|
extra = extra or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
member = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||||
|
except Huiyuan.DoesNotExist:
|
||||||
|
return None, '集团会员类型不存在'
|
||||||
|
|
||||||
|
try:
|
||||||
|
jiage = Decimal(str(jiage))
|
||||||
|
guanshifc = Decimal(str(guanshifc))
|
||||||
|
zuzhangfc = Decimal(str(zuzhangfc))
|
||||||
|
if jiage <= 0:
|
||||||
|
raise ValueError
|
||||||
|
trial_fields = _parse_trial_fields(extra, jiage, guanshifc, zuzhangfc)
|
||||||
|
except ValueError as e:
|
||||||
|
return None, str(e) if str(e) else '金额或体验配置错误'
|
||||||
|
|
||||||
|
row, created = ClubHuiyuanPrice.query.get_or_create(
|
||||||
|
club_id=club_id,
|
||||||
|
huiyuan_id=huiyuan_id,
|
||||||
|
defaults={
|
||||||
|
'jiage': jiage,
|
||||||
|
'guanshifc': guanshifc,
|
||||||
|
'zuzhangfc': zuzhangfc,
|
||||||
|
'is_enabled': True,
|
||||||
|
'bankuai_id': member.bankuai_id,
|
||||||
|
**trial_fields,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not created:
|
||||||
|
row.jiage = jiage
|
||||||
|
row.guanshifc = guanshifc
|
||||||
|
row.zuzhangfc = zuzhangfc
|
||||||
|
row.is_enabled = True
|
||||||
|
for k, v in trial_fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
|
row.save()
|
||||||
|
return {'huiyuan_id': huiyuan_id}, None
|
||||||
|
|
||||||
|
|
||||||
|
def disable_member_for_club(request, huiyuan_id):
|
||||||
|
"""俱乐部软下架。"""
|
||||||
|
if club_write_blocked(request):
|
||||||
|
return None, CLUB_WRITE_SCOPE_MSG
|
||||||
|
club_id = club_id_for_write(request)
|
||||||
|
updated = ClubHuiyuanPrice.objects.filter(
|
||||||
|
club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True,
|
||||||
|
).update(is_enabled=False)
|
||||||
|
if not updated:
|
||||||
|
return None, '该会员在本俱乐部未上架或已下架'
|
||||||
|
return {'huiyuan_id': huiyuan_id}, None
|
||||||
|
|
||||||
|
|
||||||
|
def add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None):
|
||||||
|
"""集团创建全局会员类型(仅集团后台)。"""
|
||||||
|
|
||||||
def generate_huiyuan_id():
|
def generate_huiyuan_id():
|
||||||
while True:
|
while True:
|
||||||
@@ -160,20 +288,18 @@ def add_member_for_club(request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
|||||||
if bankuai_id is not None:
|
if bankuai_id is not None:
|
||||||
create_kwargs['bankuai_id'] = bankuai_id
|
create_kwargs['bankuai_id'] = bankuai_id
|
||||||
member = Huiyuan.query.create(**create_kwargs)
|
member = Huiyuan.query.create(**create_kwargs)
|
||||||
ClubHuiyuanPrice.query.create(
|
|
||||||
club_id=club_id,
|
|
||||||
huiyuan_id=new_id,
|
|
||||||
jiage=jiage,
|
|
||||||
guanshifc=guanshifc,
|
|
||||||
zuzhangfc=zuzhangfc,
|
|
||||||
is_enabled=True,
|
|
||||||
bankuai_id=bankuai_id,
|
|
||||||
)
|
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
logger.exception('添加会员失败 club_id=%s', club_id)
|
logger.exception('创建全局会员失败')
|
||||||
return None, f'添加失败:数据冲突({e})'
|
return None, f'创建失败:{e}'
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception('添加会员失败 club_id=%s', club_id)
|
logger.exception('创建全局会员失败')
|
||||||
return None, f'添加失败:{e}'
|
return None, f'创建失败:{e}'
|
||||||
|
|
||||||
return {'huiyuan_id': member.huiyuan_id}, None
|
return {'huiyuan_id': member.huiyuan_id}, None
|
||||||
|
|
||||||
|
|
||||||
|
# 兼容旧名:俱乐部不应再调用
|
||||||
|
def add_member_for_club(request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None):
|
||||||
|
if club_write_blocked(request):
|
||||||
|
return None, CLUB_WRITE_SCOPE_MSG
|
||||||
|
return None, '俱乐部不可新建会员类型,请从集团会员 catalog 上架'
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
会员微信充值履约(回调 / 前端确认共用,幂等)。
|
会员微信充值履约(回调 / 前端确认共用,幂等)。
|
||||||
|
|
||||||
分红策略(定稿):
|
分红策略(定稿 v2):
|
||||||
- 第几次购买 cishu:本俱乐部该打手该会员,已支付成功 czjilu 笔数(不含本单)+ 1。
|
- formal_cishu:本俱乐部 + 打手 + 会员,czjilu 已支付正式单(is_trial=false)笔数 + 1。
|
||||||
与换管事无关,不看 gsfenhong,不看管事被分了几次。
|
与换管事无关;体验单不计入。
|
||||||
- cishu=1:先 duoci_fenhong(club_id+受益人+会员+cishu);无则 club_huiyuan_price 默认分成。
|
- formal_cishu=1:duoci_fenhong 或 club 默认正式分成。
|
||||||
- cishu>=2:仅 duoci_fenhong;无配置则不分(不读管事/组长用户表二次或永久分红字段)。
|
- formal_cishu>=2:仅 duoci_fenhong;无配置则不分。
|
||||||
- 价格与默认分成:仅 club_huiyuan_price + order.club_id,不读全局 huiyuan 表。
|
- 体验单:固定 trial_guanshifc / trial_zuzhangfc;不计入 formal_cishu。
|
||||||
|
|
||||||
|
财务:每笔成功履约必须 czjilu.zhuangtai=3 + 收支入账(record_wechat_income_once)。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -40,6 +42,9 @@ from users.models import UserDashou, UserGuanshi, UserZuzhang
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MEMBER_FENHONG_LEIXING = 1
|
MEMBER_FENHONG_LEIXING = 1
|
||||||
|
MEMBER_LEIXING = 1
|
||||||
|
MEMBER_PAID_STATUS = 3
|
||||||
|
MEMBER_PENDING_STATUS = 9
|
||||||
|
|
||||||
|
|
||||||
class MemberRechargeError(Exception):
|
class MemberRechargeError(Exception):
|
||||||
@@ -48,35 +53,110 @@ class MemberRechargeError(Exception):
|
|||||||
self.code = code
|
self.code = code
|
||||||
|
|
||||||
|
|
||||||
def club_huiyuan_sellable(club_id, huiyuan_id):
|
def get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=False):
|
||||||
"""仅 club_huiyuan_price 可售;未配置则不可购买。"""
|
|
||||||
club_id = club_id or CLUB_ID_DEFAULT
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
row = ClubHuiyuanPrice.query.filter(
|
qs = ClubHuiyuanPrice.query.filter(club_id=club_id, huiyuan_id=huiyuan_id)
|
||||||
club_id=club_id, huiyuan_id=huiyuan_id, is_enabled=True,
|
if require_enabled:
|
||||||
|
qs = qs.filter(is_enabled=True)
|
||||||
|
return qs.first()
|
||||||
|
|
||||||
|
|
||||||
|
def club_huiyuan_sellable(club_id, huiyuan_id, is_trial=False):
|
||||||
|
"""正式/体验是否可售及价格、天数。"""
|
||||||
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
|
row = get_club_huiyuan_row(club_id, huiyuan_id, require_enabled=True)
|
||||||
|
if not row:
|
||||||
|
return False, Decimal('0'), 0
|
||||||
|
|
||||||
|
if is_trial:
|
||||||
|
if not row.trial_enabled:
|
||||||
|
return False, Decimal('0'), 0
|
||||||
|
price = Decimal(str(row.trial_price or 0))
|
||||||
|
days = int(row.trial_days or 0)
|
||||||
|
if price <= 0 or days <= 0:
|
||||||
|
return False, Decimal('0'), 0
|
||||||
|
return True, price, days
|
||||||
|
|
||||||
|
price = Decimal(str(row.jiage or 0))
|
||||||
|
days = int(row.formal_days or 30)
|
||||||
|
if price <= 0:
|
||||||
|
return False, Decimal('0'), 0
|
||||||
|
return True, price, days
|
||||||
|
|
||||||
|
|
||||||
|
def has_formal_member_purchase(yonghuid, huiyuan_id, club_id):
|
||||||
|
"""是否已有成功支付的正式会员单(用于禁止再买体验)。"""
|
||||||
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
|
return Czjilu.query.filter(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan_id=huiyuan_id,
|
||||||
|
leixing=MEMBER_LEIXING,
|
||||||
|
zhuangtai=MEMBER_PAID_STATUS,
|
||||||
|
club_id=club_id,
|
||||||
|
is_trial=False,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def can_purchase_trial(yonghuid, huiyuan_id, club_id):
|
||||||
|
"""体验会员是否允许购买。"""
|
||||||
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
|
sellable, _, _ = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=True)
|
||||||
|
if not sellable:
|
||||||
|
return False, '本俱乐部未开启体验会员'
|
||||||
|
|
||||||
|
goumai = Huiyuangoumai.query.filter(
|
||||||
|
yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
|
||||||
).first()
|
).first()
|
||||||
if row and row.jiage and Decimal(str(row.jiage)) > 0:
|
if goumai and goumai.has_used_trial:
|
||||||
return True, Decimal(str(row.jiage))
|
return False, '体验会员仅可购买一次'
|
||||||
return False, Decimal('0')
|
|
||||||
|
if has_formal_member_purchase(yonghuid, huiyuan_id, club_id):
|
||||||
|
return False, '已购买过正式会员,不可再购买体验版'
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
def resolve_member_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
|
def validate_member_purchase(yonghuid, huiyuan_id, club_id, is_trial):
|
||||||
|
"""下单前校验。"""
|
||||||
|
is_trial = bool(is_trial)
|
||||||
|
if is_trial:
|
||||||
|
ok, msg = can_purchase_trial(yonghuid, huiyuan_id, club_id)
|
||||||
|
if not ok:
|
||||||
|
return False, msg, Decimal('0'), 0
|
||||||
|
sellable, price, days = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=True)
|
||||||
|
else:
|
||||||
|
sellable, price, days = club_huiyuan_sellable(club_id, huiyuan_id, is_trial=False)
|
||||||
|
if not sellable:
|
||||||
|
return False, '该俱乐部未配置此会员或已下架', Decimal('0'), 0
|
||||||
|
|
||||||
|
if not sellable:
|
||||||
|
return False, '会员不可购买', Decimal('0'), 0
|
||||||
|
return True, None, price, days
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
|
||||||
"""
|
"""
|
||||||
打手第几次购买该会员(= duoci_fenhong.cishu)。
|
打手第几次正式购买该会员(duoci_fenhong.cishu)。
|
||||||
同俱乐部 czjilu 已支付笔数(不含本单)+ 1。
|
仅统计 is_trial=false 的已支付 czjilu。
|
||||||
"""
|
"""
|
||||||
club_id = club_id or CLUB_ID_DEFAULT
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
prior_paid = Czjilu.query.filter(
|
prior_paid = Czjilu.query.filter(
|
||||||
yonghuid=yonghuid,
|
yonghuid=yonghuid,
|
||||||
huiyuan_id=huiyuan_id,
|
huiyuan_id=huiyuan_id,
|
||||||
leixing=1,
|
leixing=MEMBER_LEIXING,
|
||||||
zhuangtai=3,
|
zhuangtai=MEMBER_PAID_STATUS,
|
||||||
club_id=club_id,
|
club_id=club_id,
|
||||||
|
is_trial=False,
|
||||||
).exclude(dingdan_id=current_dingdan_id).count()
|
).exclude(dingdan_id=current_dingdan_id).count()
|
||||||
return prior_paid + 1
|
return prior_paid + 1
|
||||||
|
|
||||||
|
|
||||||
|
# 兼容旧名
|
||||||
|
def resolve_member_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id):
|
||||||
|
return resolve_formal_purchase_cishu(yonghuid, huiyuan_id, current_dingdan_id, club_id)
|
||||||
|
|
||||||
|
|
||||||
def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field):
|
def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field):
|
||||||
"""有行则返回金额(含 0);无行返回 None 表示未配置。"""
|
|
||||||
try:
|
try:
|
||||||
row = DuociFenhong.query.get(
|
row = DuociFenhong.query.get(
|
||||||
club_id=club_id or CLUB_ID_DEFAULT,
|
club_id=club_id or CLUB_ID_DEFAULT,
|
||||||
@@ -89,40 +169,102 @@ def _lookup_duoci_fenhong(club_id, huiyuan_id, beneficiary_id, cishu, field):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def calc_guanshi_member_fenhong(
|
def calc_guanshi_member_fenhong(huiyuan_id, guanshi_id, cishu, club_id, default_guanshi_fc):
|
||||||
huiyuan_id, guanshi_id, cishu, club_id, default_guanshi_fc,
|
|
||||||
):
|
|
||||||
"""cishu>=2 只认 duoci_fenhong;cishu=1 可回落 club 默认分成。"""
|
|
||||||
club_id = club_id or CLUB_ID_DEFAULT
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
custom = _lookup_duoci_fenhong(club_id, huiyuan_id, guanshi_id, cishu, 'guanshi_fenhong')
|
custom = _lookup_duoci_fenhong(club_id, huiyuan_id, guanshi_id, cishu, 'guanshi_fenhong')
|
||||||
if custom is not None:
|
if custom is not None:
|
||||||
return custom if custom > 0 else None
|
return custom if custom > 0 else None
|
||||||
|
|
||||||
if cishu == 1:
|
if cishu == 1:
|
||||||
base = default_guanshi_fc if default_guanshi_fc is not None else Decimal('0')
|
base = default_guanshi_fc if default_guanshi_fc is not None else Decimal('0')
|
||||||
return base if base > 0 else None
|
return base if base > 0 else None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def calc_zuzhang_member_fenhong(
|
def calc_zuzhang_member_fenhong(huiyuan_id, zuzhang_id, cishu, club_id, default_zuzhang_fc):
|
||||||
huiyuan_id, zuzhang_id, cishu, club_id, default_zuzhang_fc,
|
|
||||||
):
|
|
||||||
if not zuzhang_id:
|
if not zuzhang_id:
|
||||||
return None
|
return None
|
||||||
club_id = club_id or CLUB_ID_DEFAULT
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
custom = _lookup_duoci_fenhong(club_id, huiyuan_id, zuzhang_id, cishu, 'zuzhang_fenhong')
|
custom = _lookup_duoci_fenhong(club_id, huiyuan_id, zuzhang_id, cishu, 'zuzhang_fenhong')
|
||||||
if custom is not None:
|
if custom is not None:
|
||||||
return custom if custom > 0 else None
|
return custom if custom > 0 else None
|
||||||
|
|
||||||
if cishu == 1:
|
if cishu == 1:
|
||||||
base = default_zuzhang_fc if default_zuzhang_fc is not None else Decimal('0')
|
base = default_zuzhang_fc if default_zuzhang_fc is not None else Decimal('0')
|
||||||
return base if base > 0 else None
|
return base if base > 0 else None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _apply_member_fenhong(order, huiyuan, purchase_cishu):
|
def _calc_new_daoqi(existing, days):
|
||||||
|
"""未过期叠加;已过期从今天算。"""
|
||||||
|
now = timezone.now()
|
||||||
|
days = max(1, int(days))
|
||||||
|
if existing and existing.daoqi_time and existing.daoqi_time > now and existing.huiyuan_zhuangtai == 1:
|
||||||
|
return existing.daoqi_time + timedelta(days=days)
|
||||||
|
return now + timedelta(days=days)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_member_income(order, club_id):
|
||||||
|
"""会员充值收入入账(幂等 biz_ref=cz:dingdan_id)。"""
|
||||||
|
jine_decimal = Decimal(str(order.jine))
|
||||||
|
try:
|
||||||
|
from jituan.services.szjilu_accounting import record_wechat_income_once
|
||||||
|
ok = record_wechat_income_once(jine_decimal, club_id, biz_ref=f'cz:{order.dingdan_id}')
|
||||||
|
if not ok:
|
||||||
|
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
||||||
|
repair_wechat_income_if_missing(jine_decimal, club_id, biz_ref=f'cz:{order.dingdan_id}')
|
||||||
|
logger.info(
|
||||||
|
'会员收入入账 dingdan=%s club=%s jine=%s is_trial=%s',
|
||||||
|
order.dingdan_id, club_id, jine_decimal, getattr(order, 'is_trial', False),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error(
|
||||||
|
'会员收入入账失败 dingdan=%s club=%s jine=%s err=%s',
|
||||||
|
order.dingdan_id, club_id, order.jine, exc, exc_info=True,
|
||||||
|
)
|
||||||
|
raise MemberRechargeError('收入统计失败,请联系客服', 'income_error')
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_huiyuan_entitlement(yonghuid, huiyuan_id, huiyuan, club_id, is_trial, purchase_days):
|
||||||
|
"""开通/续期会员权益。"""
|
||||||
|
is_trial = bool(is_trial)
|
||||||
|
purchase_days = max(1, int(purchase_days))
|
||||||
|
existing = Huiyuangoumai.query.filter(
|
||||||
|
yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
|
||||||
|
).first()
|
||||||
|
is_first_buy_any = False
|
||||||
|
new_daoqi = _calc_new_daoqi(existing, purchase_days)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
existing.daoqi_time = new_daoqi
|
||||||
|
existing.huiyuan_zhuangtai = 1
|
||||||
|
existing.jieshao = huiyuan.jieshao
|
||||||
|
if is_trial:
|
||||||
|
existing.is_trial = True
|
||||||
|
existing.has_used_trial = True
|
||||||
|
else:
|
||||||
|
existing.is_trial = False
|
||||||
|
existing.save(update_fields=[
|
||||||
|
'daoqi_time', 'huiyuan_zhuangtai', 'jieshao', 'is_trial', 'has_used_trial', 'UpdateTime',
|
||||||
|
])
|
||||||
|
record = existing
|
||||||
|
else:
|
||||||
|
other = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(huiyuan_id=huiyuan_id).count()
|
||||||
|
if other == 0:
|
||||||
|
is_first_buy_any = True
|
||||||
|
resolved_club = club_id or get_user_club_id(User.query.filter(UserUID=yonghuid).first())
|
||||||
|
record = Huiyuangoumai.query.create(
|
||||||
|
yonghu_id=yonghuid,
|
||||||
|
huiyuan_id=huiyuan_id,
|
||||||
|
jieshao=huiyuan.jieshao,
|
||||||
|
huiyuan_zhuangtai=1,
|
||||||
|
daoqi_time=new_daoqi,
|
||||||
|
club_id=resolved_club,
|
||||||
|
is_trial=is_trial,
|
||||||
|
has_used_trial=is_trial,
|
||||||
|
)
|
||||||
|
return record, is_first_buy_any
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_member_fenhong(order, huiyuan, formal_cishu=None, trial_guanshi_fc=None, trial_zuzhang_fc=None):
|
||||||
if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists():
|
if Gsfenhong.query.filter(dingdan_id=order.dingdan_id).exists():
|
||||||
logger.info('会员分红已处理,跳过: %s', order.dingdan_id)
|
logger.info('会员分红已处理,跳过: %s', order.dingdan_id)
|
||||||
return
|
return
|
||||||
@@ -154,25 +296,30 @@ def _apply_member_fenhong(order, huiyuan, purchase_cishu):
|
|||||||
|
|
||||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||||
huiyuan_id = huiyuan.huiyuan_id
|
huiyuan_id = huiyuan.huiyuan_id
|
||||||
cishu = purchase_cishu
|
|
||||||
guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan_id)
|
|
||||||
order_amount = Decimal(str(order.jine))
|
order_amount = Decimal(str(order.jine))
|
||||||
|
is_trial_order = bool(getattr(order, 'is_trial', False))
|
||||||
|
|
||||||
guanshi_fenhong = calc_guanshi_member_fenhong(
|
if is_trial_order:
|
||||||
huiyuan_id, guanshi_id, cishu, club_id, guanshi_fc,
|
guanshi_fenhong = Decimal(str(trial_guanshi_fc or 0))
|
||||||
)
|
zuzhang_fenhong = Decimal(str(trial_zuzhang_fc or 0)) if zuzhang_id else Decimal('0')
|
||||||
guanshi_fenhong = guanshi_fenhong if guanshi_fenhong is not None else Decimal('0')
|
cishu = None
|
||||||
|
else:
|
||||||
zuzhang_fenhong = calc_zuzhang_member_fenhong(
|
cishu = formal_cishu or resolve_formal_purchase_cishu(
|
||||||
huiyuan_id, zuzhang_id, cishu, club_id, zuzhang_fc,
|
order.yonghuid, huiyuan_id, order.dingdan_id, club_id,
|
||||||
)
|
)
|
||||||
zuzhang_fenhong = zuzhang_fenhong if zuzhang_fenhong is not None else Decimal('0')
|
guanshi_fc, zuzhang_fc = get_huiyuan_fenchong(club_id, huiyuan_id)
|
||||||
|
guanshi_fenhong = calc_guanshi_member_fenhong(
|
||||||
|
huiyuan_id, guanshi_id, cishu, club_id, guanshi_fc,
|
||||||
|
)
|
||||||
|
guanshi_fenhong = guanshi_fenhong if guanshi_fenhong is not None else Decimal('0')
|
||||||
|
zuzhang_fenhong = calc_zuzhang_member_fenhong(
|
||||||
|
huiyuan_id, zuzhang_id, cishu, club_id, zuzhang_fc,
|
||||||
|
)
|
||||||
|
zuzhang_fenhong = zuzhang_fenhong if zuzhang_fenhong is not None else Decimal('0')
|
||||||
|
|
||||||
total = guanshi_fenhong + zuzhang_fenhong
|
total = guanshi_fenhong + zuzhang_fenhong
|
||||||
if total > order_amount:
|
if total > order_amount:
|
||||||
logger.warning(
|
logger.warning('分红总额%s超订单%s,组长置0', total, order_amount)
|
||||||
'分红总额%s超订单%s,组长置0', total, order_amount,
|
|
||||||
)
|
|
||||||
zuzhang_fenhong = Decimal('0')
|
zuzhang_fenhong = Decimal('0')
|
||||||
total = guanshi_fenhong
|
total = guanshi_fenhong
|
||||||
if total > order_amount:
|
if total > order_amount:
|
||||||
@@ -181,8 +328,8 @@ def _apply_member_fenhong(order, huiyuan, purchase_cishu):
|
|||||||
|
|
||||||
if guanshi_fenhong <= 0 and zuzhang_fenhong <= 0:
|
if guanshi_fenhong <= 0 and zuzhang_fenhong <= 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
'本次无会员分红 dingdan=%s club=%s cishu=%s',
|
'本次无会员分红 dingdan=%s club=%s cishu=%s is_trial=%s',
|
||||||
order.dingdan_id, club_id, cishu,
|
order.dingdan_id, club_id, cishu, is_trial_order,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -218,66 +365,30 @@ def _apply_member_fenhong(order, huiyuan, purchase_cishu):
|
|||||||
huiyuan_id=huiyuan_id,
|
huiyuan_id=huiyuan_id,
|
||||||
fenhong_leixing=MEMBER_FENHONG_LEIXING,
|
fenhong_leixing=MEMBER_FENHONG_LEIXING,
|
||||||
club_id=resolve_gsfenhong_club_id(dingdan_id=order.dingdan_id, czjilu=order),
|
club_id=resolve_gsfenhong_club_id(dingdan_id=order.dingdan_id, czjilu=order),
|
||||||
|
is_trial=is_trial_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
if guanshi_fenhong > 0:
|
if guanshi_fenhong > 0 and not is_trial_order and cishu is not None:
|
||||||
try:
|
try:
|
||||||
if cishu == 1:
|
if cishu == 1:
|
||||||
update_guanshi_daily_by_action(
|
update_guanshi_daily_by_action(yonghuid=guanshi_id, action=2, amount=guanshi_fenhong)
|
||||||
yonghuid=guanshi_id, action=2, amount=guanshi_fenhong,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
update_guanshi_xufei_daily(
|
update_guanshi_xufei_daily(yonghuid=guanshi_id, xufei_jine=guanshi_fenhong)
|
||||||
yonghuid=guanshi_id, xufei_jine=guanshi_fenhong,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error('管事日统计更新失败: %s', exc)
|
logger.error('管事日统计更新失败: %s', exc)
|
||||||
|
|
||||||
if zuzhang_fenhong > 0 and zuzhang_id:
|
if zuzhang_fenhong > 0 and zuzhang_id:
|
||||||
try:
|
try:
|
||||||
update_zuzhang_daily_by_action(
|
update_zuzhang_daily_by_action(yonghuid=zuzhang_id, action=2, amount=zuzhang_fenhong)
|
||||||
yonghuid=zuzhang_id, action=2, amount=zuzhang_fenhong,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error('组长日统计更新失败: %s', exc)
|
logger.error('组长日统计更新失败: %s', exc)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'会员分红完成 dingdan=%s club=%s cishu=%s 管事=%s 组长=%s',
|
'会员分红完成 dingdan=%s club=%s formal_cishu=%s is_trial=%s 管事=%s 组长=%s',
|
||||||
order.dingdan_id, club_id, cishu, guanshi_fenhong, zuzhang_fenhong,
|
order.dingdan_id, club_id, cishu, is_trial_order, guanshi_fenhong, zuzhang_fenhong,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _apply_huiyuan_goumai(yonghuid, huiyuan_id, huiyuan, club_id):
|
|
||||||
"""返回 (record, is_first_purchase_of_member, is_first_buy_any_member)。"""
|
|
||||||
existing = Huiyuangoumai.query.filter(
|
|
||||||
yonghu_id=yonghuid, huiyuan_id=huiyuan_id,
|
|
||||||
).first()
|
|
||||||
is_first_purchase = existing is None
|
|
||||||
is_first_buy_any = False
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
existing.daoqi_time += timedelta(days=30)
|
|
||||||
existing.huiyuan_zhuangtai = 1
|
|
||||||
existing.save(update_fields=['daoqi_time', 'huiyuan_zhuangtai', 'UpdateTime'])
|
|
||||||
return existing, False, False
|
|
||||||
|
|
||||||
other = Huiyuangoumai.query.filter(yonghu_id=yonghuid).exclude(
|
|
||||||
huiyuan_id=huiyuan_id,
|
|
||||||
).count()
|
|
||||||
if other == 0:
|
|
||||||
is_first_buy_any = True
|
|
||||||
|
|
||||||
record = Huiyuangoumai.query.create(
|
|
||||||
yonghu_id=yonghuid,
|
|
||||||
huiyuan_id=huiyuan_id,
|
|
||||||
jieshao=huiyuan.jieshao,
|
|
||||||
huiyuan_zhuangtai=1,
|
|
||||||
daoqi_time=timezone.now() + timedelta(days=30),
|
|
||||||
club_id=club_id or get_user_club_id(User.query.filter(UserUID=yonghuid).first()),
|
|
||||||
)
|
|
||||||
return record, is_first_purchase, is_first_buy_any
|
|
||||||
|
|
||||||
|
|
||||||
def _add_first_buy_jifen(yonghuid):
|
def _add_first_buy_jifen(yonghuid):
|
||||||
try:
|
try:
|
||||||
dashou = UserDashou.query.get(user__UserUID=yonghuid)
|
dashou = UserDashou.query.get(user__UserUID=yonghuid)
|
||||||
@@ -288,17 +399,12 @@ def _add_first_buy_jifen(yonghuid):
|
|||||||
|
|
||||||
|
|
||||||
def repair_member_entitlement_if_paid(dingdan_id):
|
def repair_member_entitlement_if_paid(dingdan_id):
|
||||||
"""
|
order = Czjilu.query.filter(dingdan_id=dingdan_id, leixing=MEMBER_LEIXING).first()
|
||||||
订单已标为已支付(zhuangtai=3)但 huiyuangoumai 缺失时补开通会员。
|
if not order or order.zhuangtai != MEMBER_PAID_STATUS or not order.huiyuan_id:
|
||||||
用于修复历史脏数据或履约中断。
|
|
||||||
"""
|
|
||||||
order = Czjilu.query.filter(dingdan_id=dingdan_id, leixing=1).first()
|
|
||||||
if not order or order.zhuangtai != 3 or not order.huiyuan_id:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
exists = Huiyuangoumai.query.filter(
|
exists = Huiyuangoumai.query.filter(
|
||||||
yonghu_id=order.yonghuid,
|
yonghu_id=order.yonghuid, huiyuan_id=order.huiyuan_id,
|
||||||
huiyuan_id=order.huiyuan_id,
|
|
||||||
).exists()
|
).exists()
|
||||||
if exists:
|
if exists:
|
||||||
return True
|
return True
|
||||||
@@ -306,44 +412,40 @@ def repair_member_entitlement_if_paid(dingdan_id):
|
|||||||
try:
|
try:
|
||||||
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
|
huiyuan = Huiyuan.query.get(huiyuan_id=order.huiyuan_id)
|
||||||
except Huiyuan.DoesNotExist:
|
except Huiyuan.DoesNotExist:
|
||||||
logger.error('补开通会员失败,会员不存在 dingdan=%s huiyuan=%s', dingdan_id, order.huiyuan_id)
|
logger.error('补开通会员失败 dingdan=%s huiyuan=%s', dingdan_id, order.huiyuan_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||||
|
days = order.purchase_days or 30
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
_apply_huiyuan_goumai(order.yonghuid, order.huiyuan_id, huiyuan, club_id)
|
_apply_huiyuan_entitlement(
|
||||||
logger.warning('已补开通会员 dingdan=%s yonghu=%s huiyuan=%s', dingdan_id, order.yonghuid, order.huiyuan_id)
|
order.yonghuid, order.huiyuan_id, huiyuan, club_id,
|
||||||
|
bool(getattr(order, 'is_trial', False)), days,
|
||||||
|
)
|
||||||
|
logger.warning('已补开通会员 dingdan=%s', dingdan_id)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'):
|
def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'):
|
||||||
"""
|
|
||||||
幂等履约:仅当 czjilu.zhuangtai==9 时执行。
|
|
||||||
已处理 (zhuangtai!=9) 直接返回 already_done=True。
|
|
||||||
"""
|
|
||||||
locked = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update()
|
locked = Czjilu.query.filter(dingdan_id=dingdan_id).select_for_update()
|
||||||
order = locked.first()
|
order = locked.first()
|
||||||
if not order:
|
if not order:
|
||||||
raise MemberRechargeError('订单不存在', 'not_found')
|
raise MemberRechargeError('订单不存在', 'not_found')
|
||||||
|
|
||||||
if order.leixing != 1:
|
if order.leixing != MEMBER_LEIXING:
|
||||||
raise MemberRechargeError('订单类型不是会员充值', 'type_error')
|
raise MemberRechargeError('订单类型不是会员充值', 'type_error')
|
||||||
|
|
||||||
if order.zhuangtai != 9:
|
if order.zhuangtai != MEMBER_PENDING_STATUS:
|
||||||
logger.info(
|
logger.info(
|
||||||
'会员订单已处理,跳过履约 dingdan=%s zhuangtai=%s source=%s',
|
'会员订单已处理,跳过履约 dingdan=%s zhuangtai=%s source=%s',
|
||||||
dingdan_id, order.zhuangtai, source,
|
dingdan_id, order.zhuangtai, source,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||||
repair_wechat_income_if_missing(
|
_record_member_income(order, club_id)
|
||||||
Decimal(str(order.jine)),
|
except MemberRechargeError:
|
||||||
getattr(order, 'club_id', None) or CLUB_ID_DEFAULT,
|
pass
|
||||||
biz_ref=f'cz:{dingdan_id}',
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error('会员补记收支失败 dingdan=%s: %s', dingdan_id, exc, exc_info=True)
|
|
||||||
return {
|
return {
|
||||||
'already_done': True,
|
'already_done': True,
|
||||||
'dingdan_id': dingdan_id,
|
'dingdan_id': dingdan_id,
|
||||||
@@ -363,62 +465,66 @@ def fulfill_member_recharge(dingdan_id, paid_amount_yuan=None, source='callback'
|
|||||||
raise MemberRechargeError(f'会员不存在: {order.huiyuan_id}', 'huiyuan_not_found')
|
raise MemberRechargeError(f'会员不存在: {order.huiyuan_id}', 'huiyuan_not_found')
|
||||||
|
|
||||||
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
club_id = getattr(order, 'club_id', None) or CLUB_ID_DEFAULT
|
||||||
|
is_trial = bool(getattr(order, 'is_trial', False))
|
||||||
|
purchase_days = order.purchase_days or (30 if not is_trial else 0)
|
||||||
|
if purchase_days <= 0:
|
||||||
|
_, _, purchase_days = club_huiyuan_sellable(club_id, order.huiyuan_id, is_trial=is_trial)
|
||||||
|
|
||||||
is_first_purchase_of_member = not Huiyuangoumai.query.filter(
|
formal_cishu = None
|
||||||
yonghu_id=order.yonghuid,
|
trial_guanshi_fc = Decimal('0')
|
||||||
huiyuan_id=order.huiyuan_id,
|
trial_zuzhang_fc = Decimal('0')
|
||||||
).exists()
|
if is_trial:
|
||||||
|
row = get_club_huiyuan_row(club_id, order.huiyuan_id)
|
||||||
|
if row:
|
||||||
|
trial_guanshi_fc = Decimal(str(row.trial_guanshifc or 0))
|
||||||
|
trial_zuzhang_fc = Decimal(str(row.trial_zuzhangfc or 0))
|
||||||
|
else:
|
||||||
|
formal_cishu = resolve_formal_purchase_cishu(
|
||||||
|
order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
|
||||||
|
)
|
||||||
|
|
||||||
# 必须在改 zhuangtai 之前算清第几次购买
|
goumai_record, is_first_buy_any = _apply_huiyuan_entitlement(
|
||||||
purchase_cishu = resolve_member_purchase_cishu(
|
order.yonghuid, order.huiyuan_id, huiyuan, club_id, is_trial, purchase_days,
|
||||||
order.yonghuid, order.huiyuan_id, order.dingdan_id, club_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 先开通会员,再记账;分红失败不得回滚会员
|
|
||||||
goumai_record, _, is_first_buy_any = _apply_huiyuan_goumai(
|
|
||||||
order.yonghuid, order.huiyuan_id, huiyuan, club_id,
|
|
||||||
)
|
)
|
||||||
if is_first_buy_any:
|
if is_first_buy_any:
|
||||||
_add_first_buy_jifen(order.yonghuid)
|
_add_first_buy_jifen(order.yonghuid)
|
||||||
|
|
||||||
order.zhuangtai = 3
|
order.zhuangtai = MEMBER_PAID_STATUS
|
||||||
order.save(update_fields=['zhuangtai'])
|
order.save(update_fields=['zhuangtai'])
|
||||||
|
|
||||||
huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
|
huiyuan.goumai_cishu = (huiyuan.goumai_cishu or 0) + 1
|
||||||
huiyuan.save(update_fields=['goumai_cishu'])
|
huiyuan.save(update_fields=['goumai_cishu'])
|
||||||
|
|
||||||
jine_decimal = Decimal(str(order.jine))
|
_record_member_income(order, club_id)
|
||||||
try:
|
|
||||||
from jituan.services.szjilu_accounting import repair_wechat_income_if_missing
|
|
||||||
repair_wechat_income_if_missing(jine_decimal, club_id, biz_ref=f'cz:{dingdan_id}')
|
|
||||||
except Exception as exc:
|
|
||||||
logger.error('会员收支统计更新失败: %s', exc, exc_info=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_apply_member_fenhong(order, huiyuan, purchase_cishu)
|
_apply_member_fenhong(
|
||||||
|
order, huiyuan,
|
||||||
|
formal_cishu=formal_cishu,
|
||||||
|
trial_guanshi_fc=trial_guanshi_fc,
|
||||||
|
trial_zuzhang_fc=trial_zuzhang_fc,
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error(
|
logger.error(
|
||||||
'会员分红失败但会员已开通 dingdan=%s err=%s',
|
'会员分红失败但会员已开通 dingdan=%s err=%s', dingdan_id, exc, exc_info=True,
|
||||||
dingdan_id, exc, exc_info=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'会员充值履约完成 dingdan=%s source=%s club=%s purchase_cishu=%s first_member=%s',
|
'会员充值履约完成 dingdan=%s source=%s club=%s is_trial=%s formal_cishu=%s jine=%s',
|
||||||
dingdan_id, source, club_id, purchase_cishu, is_first_purchase_of_member,
|
dingdan_id, source, club_id, is_trial, formal_cishu, order.jine,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
'already_done': False,
|
'already_done': False,
|
||||||
'dingdan_id': dingdan_id,
|
'dingdan_id': dingdan_id,
|
||||||
'zhuangtai': 3,
|
'zhuangtai': MEMBER_PAID_STATUS,
|
||||||
'purchase_cishu': purchase_cishu,
|
'formal_cishu': formal_cishu,
|
||||||
'is_first_purchase_of_member': is_first_purchase_of_member,
|
'is_trial': is_trial,
|
||||||
'daoqi_time': goumai_record.daoqi_time,
|
'daoqi_time': goumai_record.daoqi_time,
|
||||||
'huiyuan_id': order.huiyuan_id,
|
'huiyuan_id': order.huiyuan_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def query_wechat_v2_trade_state(out_trade_no, club_id=None):
|
def query_wechat_v2_trade_state(out_trade_no, club_id=None):
|
||||||
"""微信 V2 查单,返回 (trade_state, transaction_id) 或 (None, None)。"""
|
|
||||||
club_id = club_id or club_id_from_czjilu(out_trade_no)
|
club_id = club_id or club_id_from_czjilu(out_trade_no)
|
||||||
cfg = get_wechat_v2_config(club_id)
|
cfg = get_wechat_v2_config(club_id)
|
||||||
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
|
||||||
@@ -456,19 +562,15 @@ def query_wechat_v2_trade_state(out_trade_no, club_id=None):
|
|||||||
|
|
||||||
|
|
||||||
def confirm_member_recharge_paid(dingdan_id, yonghuid):
|
def confirm_member_recharge_paid(dingdan_id, yonghuid):
|
||||||
"""
|
|
||||||
前端支付成功确认:先查微信 SUCCESS 再幂等履约。
|
|
||||||
已履约则直接返回成功,不与回调重复入账。
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid)
|
order = Czjilu.query.get(dingdan_id=dingdan_id, yonghuid=yonghuid)
|
||||||
except Czjilu.DoesNotExist:
|
except Czjilu.DoesNotExist:
|
||||||
raise MemberRechargeError('订单不存在或不属于当前用户', 'not_found')
|
raise MemberRechargeError('订单不存在或不属于当前用户', 'not_found')
|
||||||
|
|
||||||
if order.leixing != 1:
|
if order.leixing != MEMBER_LEIXING:
|
||||||
raise MemberRechargeError('不是会员订单', 'type_error')
|
raise MemberRechargeError('不是会员订单', 'type_error')
|
||||||
|
|
||||||
if order.zhuangtai == 3:
|
if order.zhuangtai == MEMBER_PAID_STATUS:
|
||||||
goumai = Huiyuangoumai.query.filter(
|
goumai = Huiyuangoumai.query.filter(
|
||||||
yonghu_id=yonghuid, huiyuan_id=order.huiyuan_id,
|
yonghu_id=yonghuid, huiyuan_id=order.huiyuan_id,
|
||||||
).first()
|
).first()
|
||||||
@@ -482,12 +584,13 @@ def confirm_member_recharge_paid(dingdan_id, yonghuid):
|
|||||||
return {
|
return {
|
||||||
'fulfilled': False,
|
'fulfilled': False,
|
||||||
'already_done': True,
|
'already_done': True,
|
||||||
'zhuangtai': 3,
|
'zhuangtai': MEMBER_PAID_STATUS,
|
||||||
'daoqi_time': goumai.daoqi_time,
|
'daoqi_time': goumai.daoqi_time,
|
||||||
'huiyuan_id': order.huiyuan_id,
|
'huiyuan_id': order.huiyuan_id,
|
||||||
|
'is_trial': goumai.is_trial,
|
||||||
}
|
}
|
||||||
|
|
||||||
if order.zhuangtai != 9:
|
if order.zhuangtai != MEMBER_PENDING_STATUS:
|
||||||
raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error')
|
raise MemberRechargeError(f'订单状态异常: {order.zhuangtai}', 'status_error')
|
||||||
|
|
||||||
trade_state, _ = query_wechat_v2_trade_state(
|
trade_state, _ = query_wechat_v2_trade_state(
|
||||||
@@ -502,3 +605,27 @@ def confirm_member_recharge_paid(dingdan_id, yonghuid):
|
|||||||
)
|
)
|
||||||
result['fulfilled'] = not result.get('already_done')
|
result['fulfilled'] = not result.get('already_done')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def dashou_in_active_trial_period(yonghuid):
|
||||||
|
"""打手是否处于未过期的体验会员期(限制佣金提现)。"""
|
||||||
|
now = timezone.now()
|
||||||
|
return Huiyuangoumai.query.filter(
|
||||||
|
yonghu_id=yonghuid,
|
||||||
|
is_trial=True,
|
||||||
|
huiyuan_zhuangtai=1,
|
||||||
|
daoqi_time__gt=now,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def extend_member_entitlement_admin(yonghuid, huiyuan_id, days, club_id=None):
|
||||||
|
"""后台赠送正式会员(无 czjilu、无分红)。"""
|
||||||
|
try:
|
||||||
|
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||||
|
except Huiyuan.DoesNotExist:
|
||||||
|
return None, '会员类型不存在'
|
||||||
|
days = max(1, int(days))
|
||||||
|
record, _ = _apply_huiyuan_entitlement(
|
||||||
|
yonghuid, huiyuan_id, huiyuan, club_id, is_trial=False, purchase_days=days,
|
||||||
|
)
|
||||||
|
return record, None
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ from django.urls import path
|
|||||||
|
|
||||||
from backend.view import (
|
from backend.view import (
|
||||||
AddMemberView,
|
AddMemberView,
|
||||||
|
ClubMemberCatalogView,
|
||||||
|
ClubMemberDisableView,
|
||||||
|
ClubMemberEnableView,
|
||||||
FaKuanChuLiView,
|
FaKuanChuLiView,
|
||||||
FaKuanChuangJianView,
|
FaKuanChuangJianView,
|
||||||
FaKuanLieBiaoView,
|
FaKuanLieBiaoView,
|
||||||
@@ -96,5 +99,8 @@ urlpatterns = [
|
|||||||
path('houtai/hthqhylb', GetMemberListView.as_view(), name='jituan_member_list'),
|
path('houtai/hthqhylb', GetMemberListView.as_view(), name='jituan_member_list'),
|
||||||
path('houtai/htxghyxx', UpdateMemberView.as_view(), name='jituan_member_update'),
|
path('houtai/htxghyxx', UpdateMemberView.as_view(), name='jituan_member_update'),
|
||||||
path('houtai/httjhy', AddMemberView.as_view(), name='jituan_member_add'),
|
path('houtai/httjhy', AddMemberView.as_view(), name='jituan_member_add'),
|
||||||
|
path('houtai/hthycatalog', ClubMemberCatalogView.as_view(), name='jituan_member_catalog'),
|
||||||
|
path('houtai/hthysj', ClubMemberEnableView.as_view(), name='jituan_member_enable'),
|
||||||
|
path('houtai/hthyxj', ClubMemberDisableView.as_view(), name='jituan_member_disable'),
|
||||||
path('club/list', ClubListView.as_view(), name='jituan_club_list'),
|
path('club/list', ClubListView.as_view(), name='jituan_club_list'),
|
||||||
]
|
]
|
||||||
|
|||||||
38
products/migrations/0006_member_trial_fields.py
Normal file
38
products/migrations/0006_member_trial_fields.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0005_duoci_fenhong_club_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='huiyuangoumai',
|
||||||
|
name='is_trial',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='当前是否为体验权益'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='huiyuangoumai',
|
||||||
|
name='has_used_trial',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否已购买过体验版'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='czjilu',
|
||||||
|
name='is_trial',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否体验会员单'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='czjilu',
|
||||||
|
name='purchase_days',
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, null=True, verbose_name='下单锁定有效天数',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gsfenhong',
|
||||||
|
name='is_trial',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否体验会员分红'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -172,6 +172,8 @@ class Huiyuangoumai(QModel):
|
|||||||
club_id = models.CharField(
|
club_id = models.CharField(
|
||||||
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
||||||
)
|
)
|
||||||
|
is_trial = models.BooleanField(default=False, verbose_name='当前是否为体验权益')
|
||||||
|
has_used_trial = models.BooleanField(default=False, verbose_name='是否已购买过体验版')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'huiyuangoumai'
|
db_table = 'huiyuangoumai'
|
||||||
@@ -218,6 +220,10 @@ class Czjilu(QModel):
|
|||||||
club_id = models.CharField(
|
club_id = models.CharField(
|
||||||
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
||||||
)
|
)
|
||||||
|
is_trial = models.BooleanField(default=False, verbose_name='是否体验会员单')
|
||||||
|
purchase_days = models.PositiveIntegerField(
|
||||||
|
null=True, blank=True, verbose_name='下单锁定有效天数',
|
||||||
|
)
|
||||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -258,6 +264,7 @@ class Gsfenhong(QModel):
|
|||||||
club_id = models.CharField(
|
club_id = models.CharField(
|
||||||
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
max_length=16, default='xq', db_index=True, verbose_name='所属俱乐部',
|
||||||
)
|
)
|
||||||
|
is_trial = models.BooleanField(default=False, verbose_name='是否体验会员分红')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'gsfenhong'
|
db_table = 'gsfenhong'
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ class DashouHuiyuanList(APIView):
|
|||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
||||||
from jituan.services.member_recharge import club_huiyuan_sellable
|
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
|
||||||
|
|
||||||
club_id = resolve_effective_club_id(request, request.user)
|
club_id = resolve_effective_club_id(request, request.user)
|
||||||
huiyuan_queryset = Huiyuan.query.all().only(
|
huiyuan_queryset = Huiyuan.query.all().only(
|
||||||
@@ -275,15 +275,27 @@ class DashouHuiyuanList(APIView):
|
|||||||
).order_by('jiage')
|
).order_by('jiage')
|
||||||
|
|
||||||
huiyuan_list = []
|
huiyuan_list = []
|
||||||
|
yonghuid = request.user.UserUID
|
||||||
for huiyuan in huiyuan_queryset:
|
for huiyuan in huiyuan_queryset:
|
||||||
sellable, club_price = club_huiyuan_sellable(club_id, huiyuan.huiyuan_id)
|
sellable, club_price, formal_days = club_huiyuan_sellable(
|
||||||
|
club_id, huiyuan.huiyuan_id, is_trial=False,
|
||||||
|
)
|
||||||
if not sellable or not club_price:
|
if not sellable or not club_price:
|
||||||
continue
|
continue
|
||||||
|
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
|
||||||
|
trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
|
||||||
|
club_id, huiyuan.huiyuan_id, is_trial=True,
|
||||||
|
)
|
||||||
huiyuan_list.append({
|
huiyuan_list.append({
|
||||||
'id': huiyuan.huiyuan_id,
|
'id': huiyuan.huiyuan_id,
|
||||||
'mingzi': huiyuan.jieshao,
|
'mingzi': huiyuan.jieshao,
|
||||||
'jiage': float(club_price),
|
'jiage': float(club_price),
|
||||||
|
'formal_days': formal_days,
|
||||||
'jieshao': huiyuan.jtjieshao,
|
'jieshao': huiyuan.jtjieshao,
|
||||||
|
'trial_enabled': bool(trial_sellable),
|
||||||
|
'trial_price': float(trial_price) if trial_sellable else 0,
|
||||||
|
'trial_days': trial_days if trial_sellable else 0,
|
||||||
|
'can_buy_trial': bool(trial_ok and trial_sellable),
|
||||||
})
|
})
|
||||||
|
|
||||||
# 5. 返回数据
|
# 5. 返回数据
|
||||||
@@ -1381,6 +1393,7 @@ class HuiyuanGoumai(APIView):
|
|||||||
try:
|
try:
|
||||||
# 1. 获取会员ID参数
|
# 1. 获取会员ID参数
|
||||||
huiyuanid = request.data.get('huiyuanid')
|
huiyuanid = request.data.get('huiyuanid')
|
||||||
|
is_trial = bool(request.data.get('is_trial') or request.data.get('isTrial'))
|
||||||
if not huiyuanid:
|
if not huiyuanid:
|
||||||
return Response({
|
return Response({
|
||||||
'code': 400,
|
'code': 400,
|
||||||
@@ -1413,12 +1426,15 @@ class HuiyuanGoumai(APIView):
|
|||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
club_id = resolve_effective_club_id(request, request.user)
|
club_id = resolve_effective_club_id(request, request.user)
|
||||||
from jituan.services.member_recharge import club_huiyuan_sellable
|
from jituan.services.member_recharge import validate_member_purchase
|
||||||
sellable, club_price = club_huiyuan_sellable(club_id, huiyuanid)
|
|
||||||
if not sellable:
|
ok, err_msg, club_price, purchase_days = validate_member_purchase(
|
||||||
|
request.user.UserUID, huiyuanid, club_id, is_trial,
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
return Response({
|
return Response({
|
||||||
'code': 400,
|
'code': 400,
|
||||||
'message': '该俱乐部未配置此会员价格,请联系管理员'
|
'message': err_msg or '不可购买',
|
||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 5. 生成订单ID(前缀CzHY表示会员充值)
|
# 5. 生成订单ID(前缀CzHY表示会员充值)
|
||||||
@@ -1442,11 +1458,14 @@ class HuiyuanGoumai(APIView):
|
|||||||
shuoming=shuoming,
|
shuoming=shuoming,
|
||||||
huiyuan_id=huiyuanid, # 存储会员ID
|
huiyuan_id=huiyuanid, # 存储会员ID
|
||||||
club_id=club_id,
|
club_id=club_id,
|
||||||
|
is_trial=is_trial,
|
||||||
|
purchase_days=int(purchase_days),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, "
|
f"创建会员充值订单: 用户{request.user.UserUID}, 订单{dingdanid}, "
|
||||||
f"会员{huiyuanid}, 俱乐部{club_id}, 金额{jine}元")
|
f"会员{huiyuanid}, 俱乐部{club_id}, 体验={is_trial}, "
|
||||||
|
f"金额{jine}元, 天数{purchase_days}")
|
||||||
|
|
||||||
# 7. 生成微信支付参数
|
# 7. 生成微信支付参数
|
||||||
try:
|
try:
|
||||||
@@ -1477,6 +1496,8 @@ class HuiyuanGoumai(APIView):
|
|||||||
'dingdanid': dingdanid,
|
'dingdanid': dingdanid,
|
||||||
'club_id': club_id,
|
'club_id': club_id,
|
||||||
'jine': jine,
|
'jine': jine,
|
||||||
|
'is_trial': is_trial,
|
||||||
|
'purchase_days': purchase_days,
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -5862,8 +5883,11 @@ class DsqrgmdhView(APIView):
|
|||||||
if shenfen_id not in [1, 2, 3, 4]:
|
if shenfen_id not in [1, 2, 3, 4]:
|
||||||
return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'code': 400, 'msg': 'shenfen_id 必须为 1/2/3/4'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
if leixing == 1 and not huiyuan_id:
|
if leixing == 1:
|
||||||
return Response({'code': 400, 'msg': '购买会员时必须提供 huiyuan_id'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '会员购买仅支持微信支付,请使用会员充值页面',
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
if leixing == 2:
|
if leixing == 2:
|
||||||
if not yajin_jine:
|
if not yajin_jine:
|
||||||
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
|
return Response({'code': 400, 'msg': '购买押金时必须提供 yajin_jine'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|||||||
@@ -2024,6 +2024,10 @@ class TixianShenqingView(APIView):
|
|||||||
if dashou.yue < jine:
|
if dashou.yue < jine:
|
||||||
return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
|
return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
|
||||||
|
|
||||||
|
from jituan.services.member_recharge import dashou_in_active_trial_period
|
||||||
|
if dashou_in_active_trial_period(user_main.UserUID):
|
||||||
|
return {'code': 16, 'msg': '体验会员期间不可提现佣金,请升级正式会员或待体验到期'}
|
||||||
|
|
||||||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
||||||
|
|
||||||
def _check_guanshi_dividend_withdraw(self, user_main, jine):
|
def _check_guanshi_dividend_withdraw(self, user_main, jine):
|
||||||
@@ -8893,35 +8897,27 @@ class KefuUpdateDashouView(APIView):
|
|||||||
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
||||||
if not huiyuan_id:
|
if not huiyuan_id:
|
||||||
return Response({'code': 400, 'msg': '缺少会员ID'})
|
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||||
days = int(value) if value else 30 # 默认加30天
|
|
||||||
try:
|
try:
|
||||||
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||||
except Huiyuan.DoesNotExist:
|
except Huiyuan.DoesNotExist:
|
||||||
return Response({'code': 404, 'msg': '会员类型不存在'})
|
return Response({'code': 404, 'msg': '会员类型不存在'})
|
||||||
# 获取或创建购买记录
|
from jituan.services.club_user import get_user_club_id
|
||||||
record, created = Huiyuangoumai.objects.select_for_update().get_or_create(
|
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
|
||||||
yonghu_id=dashou_id,
|
player_club = get_user_club_id(dashou_user)
|
||||||
huiyuan_id=huiyuan_id,
|
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
|
||||||
defaults={
|
days = int(value) if value else int(row.formal_days or 30) if row else 30
|
||||||
'huiyuan_zhuangtai': 1,
|
record, err = extend_member_entitlement_admin(
|
||||||
'jieshao': huiyuan.jieshao,
|
dashou_id, huiyuan_id, days, player_club,
|
||||||
'daoqi_time': timezone.now() + timedelta(days=days),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if not created:
|
if err:
|
||||||
# 延长有效期
|
return Response({'code': 400, 'msg': err})
|
||||||
new_daoqi = record.daoqi_time + timedelta(days=days)
|
|
||||||
record.daoqi_time = new_daoqi
|
|
||||||
record.huiyuan_zhuangtai = 1
|
|
||||||
record.save()
|
|
||||||
# 记录修改日志
|
|
||||||
Xiugaijilu.query.create(
|
Xiugaijilu.query.create(
|
||||||
yonghuid=dashou_id,
|
yonghuid=dashou_id,
|
||||||
xiugaiid=kefu.user.Phone,
|
xiugaiid=kefu.user.Phone,
|
||||||
leixing=2,
|
leixing=2,
|
||||||
huiyuants=days,
|
huiyuants=days,
|
||||||
huiyuan_id=huiyuan_id,
|
huiyuan_id=huiyuan_id,
|
||||||
qitashuoming=f'加会员 {huiyuan_id},增加{days}天',
|
qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
|
||||||
)
|
)
|
||||||
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user