feat: 商家子客服 merchant_ops 模块与专用 API
This commit is contained in:
0
merchant_ops/services/__init__.py
Normal file
0
merchant_ops/services/__init__.py
Normal file
66
merchant_ops/services/audit.py
Normal file
66
merchant_ops/services/audit.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""操作审计。"""
|
||||
from decimal import Decimal
|
||||
|
||||
from merchant_ops.constants import ACTOR_MERCHANT, ACTOR_STAFF
|
||||
from merchant_ops.models import MerchantStaffAuditLog
|
||||
|
||||
|
||||
def write_audit(
|
||||
merchant_id,
|
||||
operator_user_id,
|
||||
operator_type,
|
||||
action,
|
||||
*,
|
||||
staff_member_id=None,
|
||||
resource_type='',
|
||||
resource_id='',
|
||||
value_before=None,
|
||||
value_after=None,
|
||||
amount=None,
|
||||
request_ip='',
|
||||
remark='',
|
||||
):
|
||||
MerchantStaffAuditLog.objects.create(
|
||||
merchant_id=merchant_id,
|
||||
operator_user_id=operator_user_id,
|
||||
operator_type=operator_type,
|
||||
staff_member_id=staff_member_id,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=str(resource_id) if resource_id else '',
|
||||
value_before=value_before,
|
||||
value_after=value_after,
|
||||
amount=Decimal(str(amount)) if amount is not None else None,
|
||||
request_ip=request_ip or '',
|
||||
remark=remark or '',
|
||||
)
|
||||
|
||||
|
||||
def audit_staff(member, action, request, **kwargs):
|
||||
write_audit(
|
||||
member.merchant_id,
|
||||
member.staff_user_id,
|
||||
ACTOR_STAFF,
|
||||
action,
|
||||
staff_member_id=member.id,
|
||||
request_ip=_client_ip(request),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def audit_owner(merchant_id, operator_id, action, request, **kwargs):
|
||||
write_audit(
|
||||
merchant_id,
|
||||
operator_id,
|
||||
ACTOR_MERCHANT,
|
||||
action,
|
||||
request_ip=_client_ip(request),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _client_ip(request):
|
||||
xff = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if xff:
|
||||
return xff.split(',')[0].strip()
|
||||
return request.META.get('REMOTE_ADDR', '') or ''
|
||||
126
merchant_ops/services/authz.py
Normal file
126
merchant_ops/services/authz.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""权限校验与成员解析。"""
|
||||
from rest_framework.exceptions import PermissionDenied, NotFound
|
||||
|
||||
from merchant_ops.constants import (
|
||||
MEMBER_STATUS_ACTIVE, MEMBER_STATUS_REMOVED, OWNER_ALL_PERMISSIONS,
|
||||
)
|
||||
from merchant_ops.models import MerchantStaffMember, MerchantStaffMemberPermission
|
||||
from merchant_ops.services.bootstrap import ensure_merchant_roles
|
||||
from users.models import UserShangjia
|
||||
|
||||
|
||||
class StaffAuthError(Exception):
|
||||
def __init__(self, code, msg):
|
||||
self.code = code
|
||||
self.msg = msg
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def is_merchant_owner(user):
|
||||
try:
|
||||
profile = user.ShopProfile
|
||||
return profile.zhuangtai == 1
|
||||
except UserShangjia.DoesNotExist:
|
||||
return False
|
||||
|
||||
|
||||
def get_active_staff_member(user):
|
||||
return MerchantStaffMember.query.filter(
|
||||
staff_user_id=user.UserUID,
|
||||
status=MEMBER_STATUS_ACTIVE,
|
||||
).select_related('role').first()
|
||||
|
||||
|
||||
def get_member_permissions(member):
|
||||
"""角色权限 + 成员覆盖。"""
|
||||
perms = set(
|
||||
member.role.role_permissions.values_list('perm_code', flat=True)
|
||||
)
|
||||
for mp in member.member_permissions.all():
|
||||
if mp.granted:
|
||||
perms.add(mp.perm_code)
|
||||
else:
|
||||
perms.discard(mp.perm_code)
|
||||
return perms
|
||||
|
||||
|
||||
def member_has_perm(member, perm_code):
|
||||
return perm_code in get_member_permissions(member)
|
||||
|
||||
|
||||
def assert_staff_perm(member, perm_code):
|
||||
if not member_has_perm(member, perm_code):
|
||||
raise StaffAuthError(403, f'无权限: {perm_code}')
|
||||
|
||||
|
||||
def resolve_staff_context(user):
|
||||
"""子客服上下文;非客服返回 None。"""
|
||||
member = get_active_staff_member(user)
|
||||
if not member:
|
||||
return None
|
||||
wallet = member.wallets.first()
|
||||
perms = list(get_member_permissions(member))
|
||||
return {
|
||||
'active': True,
|
||||
'merchant_id': member.merchant_id,
|
||||
'member_id': member.id,
|
||||
'role_id': member.role_id,
|
||||
'role_code': member.role.role_code,
|
||||
'role_name': member.role.role_name,
|
||||
'display_name': member.display_name or '',
|
||||
'permissions': perms,
|
||||
'wallet': {
|
||||
'quota_total': str(wallet.quota_total if wallet else 0),
|
||||
'quota_used': str(wallet.quota_used if wallet else 0),
|
||||
'quota_frozen': str(wallet.quota_frozen if wallet else 0),
|
||||
'quota_available': str(wallet.quota_available if wallet else 0),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def resolve_owner_or_staff(request, perm_code=None):
|
||||
"""
|
||||
返回 (mode, merchant_id, member, operator_id)
|
||||
mode: 'owner' | 'staff'
|
||||
"""
|
||||
user = request.user
|
||||
if is_merchant_owner(user):
|
||||
return 'owner', user.UserUID, None, user.UserUID
|
||||
|
||||
member = get_active_staff_member(user)
|
||||
if not member:
|
||||
raise StaffAuthError(403, '非商家老板或有效子客服')
|
||||
|
||||
if perm_code:
|
||||
assert_staff_perm(member, perm_code)
|
||||
return 'staff', member.merchant_id, member, user.UserUID
|
||||
|
||||
|
||||
def assert_not_merchant_when_bind(user):
|
||||
if is_merchant_owner(user):
|
||||
raise StaffAuthError(403, '您已是商家,无法成为子客服')
|
||||
|
||||
|
||||
def assert_no_active_staff_when_merchant_register(user):
|
||||
if MerchantStaffMember.query.filter(
|
||||
staff_user_id=user.UserUID, status=MEMBER_STATUS_ACTIVE
|
||||
).exists():
|
||||
raise StaffAuthError(403, '您当前是商家客服,请先联系商家解除后再认证商家')
|
||||
|
||||
|
||||
def assert_can_bind_staff(user):
|
||||
assert_not_merchant_when_bind(user)
|
||||
if MerchantStaffMember.query.filter(
|
||||
staff_user_id=user.UserUID, status=MEMBER_STATUS_ACTIVE
|
||||
).exists():
|
||||
raise StaffAuthError(403, '您已绑定商家客服,无法重复绑定')
|
||||
|
||||
|
||||
def get_owner_permissions():
|
||||
return list(OWNER_ALL_PERMISSIONS)
|
||||
|
||||
|
||||
def ensure_owner_merchant(merchant_id, user):
|
||||
if user.UserUID != merchant_id or not is_merchant_owner(user):
|
||||
raise StaffAuthError(403, '仅商家老板可操作')
|
||||
ensure_merchant_roles(merchant_id)
|
||||
56
merchant_ops/services/bootstrap.py
Normal file
56
merchant_ops/services/bootstrap.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""初始化权限码与商家默认角色。"""
|
||||
from merchant_ops.constants import CLUB_ID_DEFAULT, PERMISSION_DEFINITIONS, SYSTEM_ROLE_TEMPLATES
|
||||
from merchant_ops.models import (
|
||||
MerchantStaffPermission, MerchantStaffRole, MerchantStaffRolePermission,
|
||||
)
|
||||
|
||||
|
||||
def seed_global_permissions():
|
||||
for idx, (code, name) in enumerate(PERMISSION_DEFINITIONS):
|
||||
MerchantStaffPermission.objects.get_or_create(
|
||||
perm_code=code,
|
||||
defaults={'perm_name': name, 'sort_order': idx, 'status': 1},
|
||||
)
|
||||
|
||||
|
||||
def ensure_system_role_templates():
|
||||
seed_global_permissions()
|
||||
for tpl in SYSTEM_ROLE_TEMPLATES:
|
||||
role, _ = MerchantStaffRole.objects.get_or_create(
|
||||
merchant_id=None,
|
||||
role_code=tpl['role_code'],
|
||||
defaults={
|
||||
'club_id': CLUB_ID_DEFAULT,
|
||||
'role_name': tpl['role_name'],
|
||||
'is_system': True,
|
||||
'sort_order': tpl['sort_order'],
|
||||
'status': 1,
|
||||
},
|
||||
)
|
||||
for perm in tpl['permissions']:
|
||||
MerchantStaffRolePermission.objects.get_or_create(
|
||||
role=role, perm_code=perm,
|
||||
)
|
||||
|
||||
|
||||
def ensure_merchant_roles(merchant_id):
|
||||
"""为商家复制系统角色模板(若尚未存在)。"""
|
||||
ensure_system_role_templates()
|
||||
templates = MerchantStaffRole.query.filter(merchant_id=None, status=1)
|
||||
for tpl in templates:
|
||||
role, created = MerchantStaffRole.objects.get_or_create(
|
||||
merchant_id=merchant_id,
|
||||
role_code=tpl.role_code,
|
||||
defaults={
|
||||
'club_id': CLUB_ID_DEFAULT,
|
||||
'role_name': tpl.role_name,
|
||||
'is_system': True,
|
||||
'sort_order': tpl.sort_order,
|
||||
'status': 1,
|
||||
},
|
||||
)
|
||||
if created:
|
||||
for rp in tpl.role_permissions.all():
|
||||
MerchantStaffRolePermission.objects.get_or_create(
|
||||
role=role, perm_code=rp.perm_code,
|
||||
)
|
||||
211
merchant_ops/services/dispatch.py
Normal file
211
merchant_ops/services/dispatch.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""子客服派单(独立实现,不修改商家派单接口)。"""
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import time
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from merchant_ops.constants import ACTOR_STAFF, STAT_ACTION_DISPATCH
|
||||
from merchant_ops.models import MerchantOrderDispatch
|
||||
from merchant_ops.services.stats import update_staff_daily
|
||||
from merchant_ops.services.wallet import check_wallet_available, confirm_dispatch_deduct
|
||||
from orders.models import MerchantOrderExt, Order
|
||||
from orders.notice_tasks import dingdan_guangbo
|
||||
from orders.utils import calc_shangjia_order_fencheng
|
||||
from products.models import ShangpinLeixing
|
||||
from rank.models import Chenghao, DingdanBiaoqian
|
||||
from users.models import UserShangjia, UserDashou
|
||||
from users.business_models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _generate_dingdan_id():
|
||||
ts = int(time.time() * 1000)
|
||||
pid = os.getpid() % 1000
|
||||
rnd = random.randint(1000, 9999)
|
||||
return f"SJ{ts}{pid:03d}{rnd}"
|
||||
|
||||
|
||||
def staff_dispatch_order(member, staff_user, data):
|
||||
"""
|
||||
子客服发单。返回 (success, payload_dict)
|
||||
payload_dict: code/msg/data 或 dingdan
|
||||
"""
|
||||
shangpin_type_id = data.get('shangpinTypeId')
|
||||
huiyuan_id = data.get('huiyuanId')
|
||||
dingdan_jieshao = str(data.get('dingdanJieshao', '')).strip()
|
||||
dingdan_beizhu = str(data.get('dingdanBeizhu', '')).strip()
|
||||
laoban_name = str(data.get('laobanName', '')).strip()
|
||||
zhiding_uid = str(data.get('zhidingUid', '')).strip()
|
||||
jiage_str = data.get('jiage')
|
||||
label_id = data.get('labelId')
|
||||
commission_enabled = data.get('commissionEnabled', False)
|
||||
commission_value_raw = data.get('commissionValue')
|
||||
commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
|
||||
|
||||
if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]):
|
||||
return False, {'code': 400, 'msg': '缺少必填参数', 'data': None}
|
||||
|
||||
try:
|
||||
jiage = float(jiage_str)
|
||||
if jiage < 1 or jiage > 10000:
|
||||
return False, {'code': 400, 'msg': '价格需在1-10000元之间', 'data': None}
|
||||
except ValueError:
|
||||
return False, {'code': 400, 'msg': '价格格式错误', 'data': None}
|
||||
|
||||
try:
|
||||
shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id)
|
||||
except ShangpinLeixing.DoesNotExist:
|
||||
return False, {'code': 400, 'msg': '商品类型不存在', 'data': None}
|
||||
|
||||
if commission_enabled and commission_value_str:
|
||||
try:
|
||||
cv = float(commission_value_str)
|
||||
if cv > 0:
|
||||
final_yaoqiu_type = 2
|
||||
final_yongjin = Decimal(str(cv))
|
||||
final_huiyuan_id = ''
|
||||
else:
|
||||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||||
except ValueError:
|
||||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||||
else:
|
||||
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
|
||||
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
|
||||
|
||||
chenghao_obj = None
|
||||
label_id_str = str(label_id).strip() if label_id is not None else ''
|
||||
if label_id_str and label_id_str not in ('0', 'null', 'undefined'):
|
||||
try:
|
||||
lid = int(label_id_str)
|
||||
if lid > 0:
|
||||
chenghao_obj = Chenghao.query.filter(id=lid).first()
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
merchant_id = member.merchant_id
|
||||
try:
|
||||
shangjia = UserShangjia.query.get(user__UserUID=merchant_id)
|
||||
except UserShangjia.DoesNotExist:
|
||||
return False, {'code': 403, 'msg': '所属商家不存在', 'data': None}
|
||||
|
||||
if shangjia.zhuangtai != 1:
|
||||
return False, {'code': 403, 'msg': '商家状态异常', 'data': None}
|
||||
|
||||
if jiage > float(shangjia.yue):
|
||||
return False, {'code': 400, 'msg': f'商家余额不足,当前{shangjia.yue}元', 'data': None}
|
||||
|
||||
zhiding_dashou = None
|
||||
if zhiding_uid:
|
||||
try:
|
||||
zhiding_user = User.query.get(UserUID=zhiding_uid)
|
||||
zhiding_dashou = zhiding_user.DashouProfile
|
||||
if zhiding_dashou.zhanghaozhuangtai != 1:
|
||||
return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None}
|
||||
except User.DoesNotExist:
|
||||
return False, {'code': 400, 'msg': '指定用户不存在', 'data': None}
|
||||
except UserDashou.DoesNotExist:
|
||||
return False, {'code': 400, 'msg': '指定用户不是打手', 'data': None}
|
||||
|
||||
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage)
|
||||
merchant_user = User.query.get(UserUID=merchant_id)
|
||||
|
||||
dingdan = None
|
||||
try:
|
||||
with transaction.atomic():
|
||||
check_wallet_available(member, jiage)
|
||||
|
||||
dingdan = Order.query.create(
|
||||
OrderID=_generate_dingdan_id(),
|
||||
Status=7 if zhiding_dashou else 1,
|
||||
Platform=2,
|
||||
Amount=jiage,
|
||||
PlayerCommission=dashou_fencheng,
|
||||
ManagerCommission=guanshi_fencheng,
|
||||
AssignedID=zhiding_uid if zhiding_dashou else '',
|
||||
ProductID=0,
|
||||
ProductTypeID=int(shangpin_type_id),
|
||||
GrabRequirement=final_yaoqiu_type,
|
||||
CommissionReq=final_yongjin,
|
||||
MembershipID=final_huiyuan_id,
|
||||
Description=dingdan_jieshao[:2000],
|
||||
Remark=dingdan_beizhu[:1000],
|
||||
Nickname=laoban_name[:50],
|
||||
ImageURL=merchant_user.Avatar if merchant_user else '',
|
||||
User1ID=f"Sj{merchant_id}",
|
||||
)
|
||||
|
||||
MerchantOrderExt.query.create(
|
||||
Order=dingdan,
|
||||
MerchantID=merchant_id,
|
||||
MerchantNickname=shangjia.nicheng,
|
||||
)
|
||||
|
||||
if chenghao_obj:
|
||||
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj)
|
||||
|
||||
UserShangjia.query.filter(id=shangjia.id).update(
|
||||
fabu=F('fabu') + 1,
|
||||
yue=F('yue') - Decimal(str(jiage)),
|
||||
jinridingdan=F('jinridingdan') + 1,
|
||||
jinriliushui=F('jinriliushui') + Decimal(str(jiage)),
|
||||
jinyuedingdan=F('jinyuedingdan') + 1,
|
||||
jinyueliushui=F('jinyueliushui') + Decimal(str(jiage)),
|
||||
)
|
||||
|
||||
update_shangjia_daily(merchant_id, Decimal(str(jiage)), STAT_ACTION_DISPATCH)
|
||||
update_staff_daily(
|
||||
merchant_id, member.staff_user_id, member.id, jiage, STAT_ACTION_DISPATCH,
|
||||
)
|
||||
|
||||
MerchantOrderDispatch.objects.create(
|
||||
order_id=dingdan.OrderID,
|
||||
merchant_id=merchant_id,
|
||||
dispatch_actor_type=ACTOR_STAFF,
|
||||
dispatch_actor_id=member.staff_user_id,
|
||||
staff_member_id=member.id,
|
||||
)
|
||||
|
||||
confirm_dispatch_deduct(member, jiage, dingdan.OrderID, member.staff_user_id)
|
||||
|
||||
from merchant_ops.models import MerchantStaffMember
|
||||
MerchantStaffMember.query.filter(id=member.id).update(
|
||||
today_dispatch_count=F('today_dispatch_count') + 1,
|
||||
today_dispatch_amount=F('today_dispatch_amount') + Decimal(str(jiage)),
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
return False, {'code': 400, 'msg': str(e), 'data': None}
|
||||
except Exception as e:
|
||||
logger.error(f'子客服派单失败: {e}', exc_info=True)
|
||||
return False, {'code': 500, 'msg': '订单创建失败', 'data': None}
|
||||
|
||||
try:
|
||||
from orders.views import ShangjiaPaifaView
|
||||
view = ShangjiaPaifaView()
|
||||
game_type = view._get_game_type_name(dingdan.ProductTypeID)
|
||||
except Exception:
|
||||
game_type = '游戏订单'
|
||||
|
||||
try:
|
||||
dingdan_guangbo.delay({
|
||||
'OrderID': dingdan.OrderID,
|
||||
'game_type': game_type,
|
||||
'amount': str(dingdan.Amount),
|
||||
'order_desc': (dingdan.Description or '')[:50],
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f'广播任务失败: {e}')
|
||||
|
||||
return True, {
|
||||
'code': 200,
|
||||
'msg': '派单成功',
|
||||
'data': {'dingdan_id': dingdan.OrderID, 'jine': str(dingdan.Amount)},
|
||||
}
|
||||
108
merchant_ops/services/stats.py
Normal file
108
merchant_ops/services/stats.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""子客服日统计 + 商家日统计双写。"""
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
|
||||
from backend.utils import update_shangjia_daily
|
||||
from merchant_ops.constants import STAT_ACTION_DISPATCH, STAT_ACTION_REFUND, STAT_ACTION_SETTLE
|
||||
from merchant_ops.models import MerchantStaffDailyStats
|
||||
|
||||
|
||||
def _action_fields(action, amount):
|
||||
amount = Decimal(str(amount))
|
||||
if action == STAT_ACTION_DISPATCH:
|
||||
return {'dispatch_count': 1, 'dispatch_amount': amount}
|
||||
if action == STAT_ACTION_SETTLE:
|
||||
return {'settle_count': 1, 'settle_amount': amount}
|
||||
if action == STAT_ACTION_REFUND:
|
||||
return {'refund_count': 1, 'refund_amount': amount}
|
||||
return {}
|
||||
|
||||
|
||||
def update_staff_daily(merchant_id, staff_user_id, member_id, amount, action):
|
||||
"""子客服日统计;老板操作不写此表。"""
|
||||
amount = Decimal(str(amount)) if amount else Decimal('0.00')
|
||||
fields = _action_fields(action, amount)
|
||||
if not fields:
|
||||
return
|
||||
|
||||
today = date.today()
|
||||
with transaction.atomic():
|
||||
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
|
||||
merchant_id=merchant_id,
|
||||
staff_user_id=staff_user_id,
|
||||
stat_date=today,
|
||||
defaults={
|
||||
'club_id': 'xq',
|
||||
'member_id': member_id,
|
||||
'year': today.year,
|
||||
'month': today.month,
|
||||
'day': today.day,
|
||||
},
|
||||
)
|
||||
update_kwargs = {}
|
||||
for k, v in fields.items():
|
||||
if k.endswith('_count'):
|
||||
update_kwargs[k] = F(k) + int(v)
|
||||
else:
|
||||
update_kwargs[k] = F(k) + v
|
||||
MerchantStaffDailyStats.query.filter(id=stat.id).update(**update_kwargs)
|
||||
|
||||
|
||||
def update_merchant_and_staff_stats(merchant_id, staff_member, amount, action):
|
||||
"""商家日统计必写;子客服则双写。"""
|
||||
update_shangjia_daily(merchant_id, amount, action)
|
||||
if staff_member:
|
||||
update_staff_daily(
|
||||
merchant_id,
|
||||
staff_member.staff_user_id,
|
||||
staff_member.id,
|
||||
amount,
|
||||
action,
|
||||
)
|
||||
|
||||
|
||||
def bump_cancel_count(merchant_id, staff_member):
|
||||
if not staff_member:
|
||||
return
|
||||
today = date.today()
|
||||
with transaction.atomic():
|
||||
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
|
||||
merchant_id=merchant_id,
|
||||
staff_user_id=staff_member.staff_user_id,
|
||||
stat_date=today,
|
||||
defaults={
|
||||
'club_id': 'xq',
|
||||
'member_id': staff_member.id,
|
||||
'year': today.year,
|
||||
'month': today.month,
|
||||
'day': today.day,
|
||||
},
|
||||
)
|
||||
MerchantStaffDailyStats.query.filter(id=stat.id).update(
|
||||
cancel_count=F('cancel_count') + 1,
|
||||
)
|
||||
|
||||
|
||||
def bump_penalty_count(merchant_id, staff_member):
|
||||
if not staff_member:
|
||||
return
|
||||
today = date.today()
|
||||
with transaction.atomic():
|
||||
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
|
||||
merchant_id=merchant_id,
|
||||
staff_user_id=staff_member.staff_user_id,
|
||||
stat_date=today,
|
||||
defaults={
|
||||
'club_id': 'xq',
|
||||
'member_id': staff_member.id,
|
||||
'year': today.year,
|
||||
'month': today.month,
|
||||
'day': today.day,
|
||||
},
|
||||
)
|
||||
MerchantStaffDailyStats.query.filter(id=stat.id).update(
|
||||
penalty_count=F('penalty_count') + 1,
|
||||
)
|
||||
77
merchant_ops/services/wallet.py
Normal file
77
merchant_ops/services/wallet.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""资金池:划额度、派单扣减。"""
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
|
||||
from merchant_ops.constants import WALLET_ALLOCATE, WALLET_DISPATCH_CONFIRM
|
||||
from merchant_ops.models import MerchantStaffWallet, MerchantStaffWalletLedger
|
||||
|
||||
|
||||
def get_or_create_wallet(member):
|
||||
wallet, _ = MerchantStaffWallet.objects.get_or_create(
|
||||
merchant_id=member.merchant_id,
|
||||
staff_user_id=member.staff_user_id,
|
||||
defaults={'member': member},
|
||||
)
|
||||
return wallet
|
||||
|
||||
|
||||
def allocate_quota(member, amount, operator_id, remark=''):
|
||||
amount = Decimal(str(amount))
|
||||
if amount <= 0:
|
||||
raise ValueError('划额度必须大于0')
|
||||
|
||||
with transaction.atomic():
|
||||
wallet = MerchantStaffWallet.objects.select_for_update().get(
|
||||
merchant_id=member.merchant_id,
|
||||
staff_user_id=member.staff_user_id,
|
||||
)
|
||||
wallet.quota_total = F('quota_total') + amount
|
||||
wallet.save(update_fields=['quota_total'])
|
||||
wallet.refresh_from_db()
|
||||
MerchantStaffWalletLedger.objects.create(
|
||||
wallet=wallet,
|
||||
merchant_id=member.merchant_id,
|
||||
staff_user_id=member.staff_user_id,
|
||||
change_type=WALLET_ALLOCATE,
|
||||
amount=amount,
|
||||
balance_after=wallet.quota_available,
|
||||
operator_id=operator_id,
|
||||
remark=remark,
|
||||
)
|
||||
return wallet
|
||||
|
||||
|
||||
def check_wallet_available(member, amount):
|
||||
amount = Decimal(str(amount))
|
||||
wallet = get_or_create_wallet(member)
|
||||
if amount > wallet.quota_available:
|
||||
raise ValueError('客服可用额度不足')
|
||||
|
||||
|
||||
def confirm_dispatch_deduct(member, amount, order_id, operator_id):
|
||||
"""派单成功:扣客服额度。"""
|
||||
amount = Decimal(str(amount))
|
||||
with transaction.atomic():
|
||||
wallet = MerchantStaffWallet.objects.select_for_update().get(
|
||||
merchant_id=member.merchant_id,
|
||||
staff_user_id=member.staff_user_id,
|
||||
)
|
||||
available = wallet.quota_total - wallet.quota_used - wallet.quota_frozen
|
||||
if amount > available:
|
||||
raise ValueError('客服可用额度不足')
|
||||
wallet.quota_used = F('quota_used') + amount
|
||||
wallet.save(update_fields=['quota_used'])
|
||||
wallet.refresh_from_db()
|
||||
MerchantStaffWalletLedger.objects.create(
|
||||
wallet=wallet,
|
||||
merchant_id=member.merchant_id,
|
||||
staff_user_id=member.staff_user_id,
|
||||
change_type=WALLET_DISPATCH_CONFIRM,
|
||||
amount=-amount,
|
||||
balance_after=wallet.quota_available,
|
||||
related_order_id=order_id,
|
||||
operator_id=operator_id,
|
||||
remark='派单扣额度',
|
||||
)
|
||||
Reference in New Issue
Block a user