feat: 商家子客服 merchant_ops 模块与专用 API
This commit is contained in:
@@ -40,6 +40,7 @@ INSTALLED_APPS = [
|
||||
'utils',
|
||||
'shop',
|
||||
'rank',
|
||||
'merchant_ops',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
0
merchant_ops/__init__.py
Normal file
0
merchant_ops/__init__.py
Normal file
7
merchant_ops/apps.py
Normal file
7
merchant_ops/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MerchantOpsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'merchant_ops'
|
||||
verbose_name = '商家子客服'
|
||||
88
merchant_ops/constants.py
Normal file
88
merchant_ops/constants.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""商家子客服权限码与常量(码表冻结,只增不改)。"""
|
||||
|
||||
CLUB_ID_DEFAULT = 'xq'
|
||||
|
||||
MEMBER_STATUS_ACTIVE = 1
|
||||
MEMBER_STATUS_DISABLED = 0
|
||||
MEMBER_STATUS_REMOVED = 2
|
||||
|
||||
INVITE_STATUS_UNUSED = 0
|
||||
INVITE_STATUS_USED = 1
|
||||
INVITE_STATUS_REVOKED = 2
|
||||
INVITE_STATUS_EXPIRED = 3
|
||||
|
||||
ACTOR_MERCHANT = 'MERCHANT'
|
||||
ACTOR_STAFF = 'STAFF'
|
||||
|
||||
WALLET_ALLOCATE = 'ALLOCATE'
|
||||
WALLET_DISPATCH_FREEZE = 'DISPATCH_FREEZE'
|
||||
WALLET_DISPATCH_CONFIRM = 'DISPATCH_CONFIRM'
|
||||
WALLET_REFUND_RELEASE = 'REFUND_RELEASE'
|
||||
WALLET_REVOKE = 'REVOKE'
|
||||
WALLET_ADJUST = 'ADJUST'
|
||||
|
||||
STAT_ACTION_DISPATCH = 1
|
||||
STAT_ACTION_SETTLE = 2
|
||||
STAT_ACTION_REFUND = 3
|
||||
|
||||
# perm_code -> 中文名
|
||||
PERMISSION_DEFINITIONS = [
|
||||
('staff_manage', '子客服管理'),
|
||||
('role_manage', '角色管理'),
|
||||
('wallet_allocate', '划额度'),
|
||||
('wallet_view', '查看额度'),
|
||||
('finance_view', '查看商家余额'),
|
||||
('order_dispatch', '派单发单'),
|
||||
('order_view_all', '查看全部订单'),
|
||||
('order_view_self', '只看自己派的单'),
|
||||
('order_detail', '订单详情'),
|
||||
('order_settle', '结单'),
|
||||
('order_refund_apply', '申请退款'),
|
||||
('order_cancel', '撤销订单'),
|
||||
('order_change_player', '换打手'),
|
||||
('penalty_apply', '申请罚单'),
|
||||
('penalty_manage', '改撤罚单'),
|
||||
('penalty_view', '查看罚单'),
|
||||
('dashou_ban', '封禁解封打手'),
|
||||
('template_manage', '派单模板'),
|
||||
('audit_view', '操作日志'),
|
||||
('rank_view', '客服排行榜'),
|
||||
('im_chat', '订单IM'),
|
||||
('stats_view', '统计数据'),
|
||||
]
|
||||
|
||||
# 系统预置角色 -> 默认权限
|
||||
SYSTEM_ROLE_TEMPLATES = [
|
||||
{
|
||||
'role_code': 'CHIEF',
|
||||
'role_name': '总管',
|
||||
'sort_order': 100,
|
||||
'permissions': [
|
||||
'staff_manage', 'wallet_allocate', 'wallet_view', 'finance_view',
|
||||
'order_dispatch', 'order_view_all', 'order_detail', 'order_settle',
|
||||
'order_refund_apply', 'order_cancel', 'order_change_player',
|
||||
'penalty_apply', 'penalty_manage', 'penalty_view', 'dashou_ban',
|
||||
'template_manage', 'audit_view', 'rank_view', 'im_chat', 'stats_view',
|
||||
],
|
||||
},
|
||||
{
|
||||
'role_code': 'FINANCE',
|
||||
'role_name': '财务',
|
||||
'sort_order': 80,
|
||||
'permissions': [
|
||||
'wallet_allocate', 'wallet_view', 'finance_view', 'order_view_all',
|
||||
'order_detail', 'penalty_view', 'audit_view', 'rank_view', 'stats_view',
|
||||
],
|
||||
},
|
||||
{
|
||||
'role_code': 'DISPATCHER',
|
||||
'role_name': '派单员',
|
||||
'sort_order': 60,
|
||||
'permissions': [
|
||||
'wallet_view', 'order_dispatch', 'order_view_self', 'order_detail',
|
||||
'rank_view', 'im_chat', 'stats_view',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
OWNER_ALL_PERMISSIONS = [code for code, _ in PERMISSION_DEFINITIONS]
|
||||
52
merchant_ops/helpers.py
Normal file
52
merchant_ops/helpers.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""视图辅助:以商家身份代理调用现有订单 View(不修改原接口)。"""
|
||||
import logging
|
||||
|
||||
from backend.utils import pick_order_id
|
||||
from merchant_ops.services.authz import StaffAuthError, member_has_perm
|
||||
from orders.models import MerchantOrderExt, MerchantOrderDispatch
|
||||
from users.business_models import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_merchant_user(merchant_id):
|
||||
return User.query.get(UserUID=merchant_id)
|
||||
|
||||
|
||||
def staff_can_access_order(member, order_id, perm_all='order_view_all', perm_self='order_view_self'):
|
||||
ext = MerchantOrderExt.query.filter(
|
||||
MerchantID=member.merchant_id,
|
||||
Order__OrderID=order_id,
|
||||
).first()
|
||||
if not ext:
|
||||
return False, None
|
||||
if member_has_perm(member, perm_all):
|
||||
return True, ext
|
||||
if member_has_perm(member, perm_self):
|
||||
disp = MerchantOrderDispatch.query.filter(order_id=order_id).first()
|
||||
if disp and disp.staff_member_id == member.id:
|
||||
return True, ext
|
||||
return False, ext
|
||||
|
||||
|
||||
def proxy_merchant_order_view(request, member, view_class, perm_code):
|
||||
"""子客服通过商家 User 代理调用现有 View.post。"""
|
||||
if not member_has_perm(member, perm_code):
|
||||
raise StaffAuthError(403, f'无权限: {perm_code}')
|
||||
|
||||
order_id = pick_order_id(request.data)
|
||||
if order_id:
|
||||
ok, _ = staff_can_access_order(member, order_id)
|
||||
if not ok:
|
||||
raise StaffAuthError(403, '无权操作该订单')
|
||||
|
||||
merchant_user = get_merchant_user(member.merchant_id)
|
||||
old_user = request.user
|
||||
request.user = merchant_user
|
||||
request._staff_member = member
|
||||
try:
|
||||
return view_class().post(request)
|
||||
finally:
|
||||
request.user = old_user
|
||||
if hasattr(request, '_staff_member'):
|
||||
delattr(request, '_staff_member')
|
||||
0
merchant_ops/management/__init__.py
Normal file
0
merchant_ops/management/__init__.py
Normal file
0
merchant_ops/management/commands/__init__.py
Normal file
0
merchant_ops/management/commands/__init__.py
Normal file
11
merchant_ops/management/commands/seed_merchant_staff.py
Normal file
11
merchant_ops/management/commands/seed_merchant_staff.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from merchant_ops.services.bootstrap import ensure_system_role_templates
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '初始化商家子客服权限码与系统角色模板(可重复执行)'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
ensure_system_role_templates()
|
||||
self.stdout.write(self.style.SUCCESS('merchant_staff 权限与角色模板已就绪'))
|
||||
207
merchant_ops/migrations/0001_initial.py
Normal file
207
merchant_ops/migrations/0001_initial.py
Normal file
@@ -0,0 +1,207 @@
|
||||
# Generated manually for merchant_ops
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffPermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('perm_code', models.CharField(max_length=64, unique=True, verbose_name='权限码')),
|
||||
('perm_name', models.CharField(max_length=64, verbose_name='权限名称')),
|
||||
('sort_order', models.IntegerField(default=0, verbose_name='排序')),
|
||||
('status', models.IntegerField(default=1, verbose_name='1启用0停用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_permission', 'verbose_name': '商家客服权限字典'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(default='xq', max_length=16, verbose_name='Club')),
|
||||
('merchant_id', models.CharField(blank=True, db_index=True, max_length=7, null=True, verbose_name='商家ID')),
|
||||
('role_code', models.CharField(max_length=32, verbose_name='角色码')),
|
||||
('role_name', models.CharField(max_length=64, verbose_name='角色名')),
|
||||
('is_system', models.BooleanField(default=False, verbose_name='系统预置')),
|
||||
('sort_order', models.IntegerField(default=0)),
|
||||
('status', models.IntegerField(default=1, verbose_name='1启用0停用')),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_role', 'verbose_name': '商家客服角色', 'unique_together': {('merchant_id', 'role_code')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffInvite',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(default='xq', max_length=16)),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7, verbose_name='商家ID')),
|
||||
('invite_code', models.CharField(max_length=32, unique=True, verbose_name='邀请码')),
|
||||
('status', models.IntegerField(default=0, verbose_name='0未用1已用2作废3过期')),
|
||||
('created_by', models.CharField(max_length=7, verbose_name='创建人')),
|
||||
('used_by', models.CharField(blank=True, max_length=7, null=True)),
|
||||
('used_at', models.DateTimeField(blank=True, null=True)),
|
||||
('expire_at', models.DateTimeField(blank=True, null=True)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='merchant_ops.merchantstaffrole', verbose_name='预绑角色')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_invite'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffMember',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(default='xq', max_length=16)),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('staff_user_id', models.CharField(db_index=True, max_length=7)),
|
||||
('status', models.IntegerField(default=1, verbose_name='1正常0停用2已移除')),
|
||||
('display_name', models.CharField(blank=True, default='', max_length=64)),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(blank=True, null=True)),
|
||||
('today_dispatch_count', models.IntegerField(default=0)),
|
||||
('today_dispatch_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
('invited_by_code', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='merchant_ops.merchantstaffinvite')),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='merchant_ops.merchantstaffrole')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_member', 'unique_together': {('merchant_id', 'staff_user_id')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffRolePermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('perm_code', models.CharField(db_index=True, max_length=64)),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='role_permissions', to='merchant_ops.merchantstaffrole')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_role_permission', 'unique_together': {('role', 'perm_code')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffMemberPermission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('perm_code', models.CharField(max_length=64)),
|
||||
('granted', models.BooleanField(default=True, verbose_name='True授予False禁止')),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='member_permissions', to='merchant_ops.merchantstaffmember')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_member_permission', 'unique_together': {('member', 'perm_code')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffWallet',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('staff_user_id', models.CharField(db_index=True, max_length=7)),
|
||||
('quota_total', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('quota_used', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('quota_frozen', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wallets', to='merchant_ops.merchantstaffmember')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_wallet', 'unique_together': {('merchant_id', 'staff_user_id')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffWalletLedger',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('staff_user_id', models.CharField(db_index=True, max_length=7)),
|
||||
('change_type', models.CharField(max_length=32)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('balance_after', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('related_order_id', models.CharField(blank=True, max_length=32, null=True)),
|
||||
('operator_id', models.CharField(max_length=7)),
|
||||
('remark', models.CharField(blank=True, default='', max_length=255)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('wallet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ledgers', to='merchant_ops.merchantstaffwallet')),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_wallet_ledger'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffDailyStats',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(default='xq', max_length=16)),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('staff_user_id', models.CharField(db_index=True, max_length=7)),
|
||||
('member_id', models.BigIntegerField(db_index=True)),
|
||||
('stat_date', models.DateField(db_index=True)),
|
||||
('year', models.PositiveSmallIntegerField()),
|
||||
('month', models.PositiveSmallIntegerField()),
|
||||
('day', models.PositiveSmallIntegerField()),
|
||||
('dispatch_count', models.IntegerField(default=0)),
|
||||
('dispatch_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('settle_count', models.IntegerField(default=0)),
|
||||
('settle_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('refund_count', models.IntegerField(default=0)),
|
||||
('refund_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)),
|
||||
('cancel_count', models.IntegerField(default=0)),
|
||||
('penalty_count', models.IntegerField(default=0)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_ri_tongji', 'unique_together': {('merchant_id', 'staff_user_id', 'stat_date')}},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantOrderDispatch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('order_id', models.CharField(db_index=True, max_length=32, unique=True)),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('dispatch_actor_type', models.CharField(max_length=16, verbose_name='MERCHANT/STAFF')),
|
||||
('dispatch_actor_id', models.CharField(max_length=7)),
|
||||
('staff_member_id', models.BigIntegerField(blank=True, db_index=True, null=True)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_order_dispatch'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantStaffAuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('operator_user_id', models.CharField(db_index=True, max_length=7)),
|
||||
('operator_type', models.CharField(max_length=16, verbose_name='MERCHANT_OWNER/STAFF')),
|
||||
('staff_member_id', models.BigIntegerField(blank=True, null=True)),
|
||||
('action', models.CharField(db_index=True, max_length=64)),
|
||||
('resource_type', models.CharField(blank=True, default='', max_length=32)),
|
||||
('resource_id', models.CharField(blank=True, default='', max_length=64)),
|
||||
('value_before', models.JSONField(blank=True, null=True)),
|
||||
('value_after', models.JSONField(blank=True, null=True)),
|
||||
('amount', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)),
|
||||
('request_ip', models.CharField(blank=True, default='', max_length=64)),
|
||||
('remark', models.CharField(blank=True, default='', max_length=500)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_staff_audit_log'},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MerchantDispatchTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('merchant_id', models.CharField(db_index=True, max_length=7)),
|
||||
('template_name', models.CharField(max_length=128)),
|
||||
('template_type', models.CharField(default='EXPRESS', max_length=16)),
|
||||
('payload_json', models.JSONField(default=dict)),
|
||||
('created_by', models.CharField(max_length=7)),
|
||||
('created_by_type', models.CharField(default='MERCHANT', max_length=16)),
|
||||
('status', models.IntegerField(default=1)),
|
||||
('use_count', models.IntegerField(default=0)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={'db_table': 'merchant_dispatch_template'},
|
||||
),
|
||||
]
|
||||
0
merchant_ops/migrations/__init__.py
Normal file
0
merchant_ops/migrations/__init__.py
Normal file
224
merchant_ops/models.py
Normal file
224
merchant_ops/models.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from gvsdsdk.model_base import QModel
|
||||
|
||||
|
||||
class MerchantStaffPermission(QModel):
|
||||
"""权限字典(全局码表)"""
|
||||
perm_code = models.CharField(max_length=64, unique=True, verbose_name='权限码')
|
||||
perm_name = models.CharField(max_length=64, verbose_name='权限名称')
|
||||
sort_order = models.IntegerField(default=0, verbose_name='排序')
|
||||
status = models.IntegerField(default=1, verbose_name='1启用0停用')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_permission'
|
||||
verbose_name = '商家客服权限字典'
|
||||
|
||||
|
||||
class MerchantStaffRole(QModel):
|
||||
"""商家内角色(merchant_id 为空表示系统模板)"""
|
||||
club_id = models.CharField(max_length=16, default='xq', verbose_name='Club')
|
||||
merchant_id = models.CharField(
|
||||
max_length=7, null=True, blank=True, db_index=True, verbose_name='商家ID'
|
||||
)
|
||||
role_code = models.CharField(max_length=32, verbose_name='角色码')
|
||||
role_name = models.CharField(max_length=64, verbose_name='角色名')
|
||||
is_system = models.BooleanField(default=False, verbose_name='系统预置')
|
||||
sort_order = models.IntegerField(default=0)
|
||||
status = models.IntegerField(default=1, verbose_name='1启用0停用')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_role'
|
||||
verbose_name = '商家客服角色'
|
||||
unique_together = [['merchant_id', 'role_code']]
|
||||
|
||||
|
||||
class MerchantStaffRolePermission(QModel):
|
||||
role = models.ForeignKey(
|
||||
MerchantStaffRole, on_delete=models.CASCADE, related_name='role_permissions'
|
||||
)
|
||||
perm_code = models.CharField(max_length=64, db_index=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_role_permission'
|
||||
unique_together = [['role', 'perm_code']]
|
||||
|
||||
|
||||
class MerchantStaffInvite(QModel):
|
||||
club_id = models.CharField(max_length=16, default='xq')
|
||||
merchant_id = models.CharField(max_length=7, db_index=True, verbose_name='商家ID')
|
||||
invite_code = models.CharField(max_length=32, unique=True, verbose_name='邀请码')
|
||||
role = models.ForeignKey(
|
||||
MerchantStaffRole, null=True, blank=True, on_delete=models.SET_NULL,
|
||||
verbose_name='预绑角色'
|
||||
)
|
||||
status = models.IntegerField(default=0, verbose_name='0未用1已用2作废3过期')
|
||||
created_by = models.CharField(max_length=7, verbose_name='创建人')
|
||||
used_by = models.CharField(max_length=7, null=True, blank=True)
|
||||
used_at = models.DateTimeField(null=True, blank=True)
|
||||
expire_at = models.DateTimeField(null=True, blank=True)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_invite'
|
||||
indexes = [
|
||||
models.Index(fields=['merchant_id', 'status', '-CreateTime']),
|
||||
]
|
||||
|
||||
|
||||
class MerchantStaffMember(QModel):
|
||||
club_id = models.CharField(max_length=16, default='xq')
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
staff_user_id = models.CharField(max_length=7, db_index=True)
|
||||
role = models.ForeignKey(MerchantStaffRole, on_delete=models.PROTECT)
|
||||
status = models.IntegerField(default=1, verbose_name='1正常0停用2已移除')
|
||||
display_name = models.CharField(max_length=64, blank=True, default='')
|
||||
invited_by_code = models.ForeignKey(
|
||||
MerchantStaffInvite, null=True, blank=True, on_delete=models.SET_NULL
|
||||
)
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
revoked_at = models.DateTimeField(null=True, blank=True)
|
||||
today_dispatch_count = models.IntegerField(default=0)
|
||||
today_dispatch_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_member'
|
||||
unique_together = [['merchant_id', 'staff_user_id']]
|
||||
indexes = [
|
||||
models.Index(fields=['staff_user_id', 'status']),
|
||||
]
|
||||
|
||||
|
||||
class MerchantStaffMemberPermission(QModel):
|
||||
member = models.ForeignKey(
|
||||
MerchantStaffMember, on_delete=models.CASCADE, related_name='member_permissions'
|
||||
)
|
||||
perm_code = models.CharField(max_length=64)
|
||||
granted = models.BooleanField(default=True, verbose_name='True授予False禁止')
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_member_permission'
|
||||
unique_together = [['member', 'perm_code']]
|
||||
|
||||
|
||||
class MerchantStaffWallet(QModel):
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
staff_user_id = models.CharField(max_length=7, db_index=True)
|
||||
member = models.ForeignKey(
|
||||
MerchantStaffMember, on_delete=models.CASCADE, related_name='wallets'
|
||||
)
|
||||
quota_total = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
quota_used = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
quota_frozen = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_wallet'
|
||||
unique_together = [['merchant_id', 'staff_user_id']]
|
||||
|
||||
@property
|
||||
def quota_available(self):
|
||||
return self.quota_total - self.quota_used - self.quota_frozen
|
||||
|
||||
|
||||
class MerchantStaffWalletLedger(QModel):
|
||||
wallet = models.ForeignKey(MerchantStaffWallet, on_delete=models.CASCADE, related_name='ledgers')
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
staff_user_id = models.CharField(max_length=7, db_index=True)
|
||||
change_type = models.CharField(max_length=32)
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
balance_after = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
related_order_id = models.CharField(max_length=32, null=True, blank=True)
|
||||
operator_id = models.CharField(max_length=7)
|
||||
remark = models.CharField(max_length=255, blank=True, default='')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_wallet_ledger'
|
||||
indexes = [models.Index(fields=['merchant_id', '-CreateTime'])]
|
||||
|
||||
|
||||
class MerchantStaffDailyStats(QModel):
|
||||
club_id = models.CharField(max_length=16, default='xq')
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
staff_user_id = models.CharField(max_length=7, db_index=True)
|
||||
member_id = models.BigIntegerField(db_index=True)
|
||||
stat_date = models.DateField(db_index=True)
|
||||
year = models.PositiveSmallIntegerField()
|
||||
month = models.PositiveSmallIntegerField()
|
||||
day = models.PositiveSmallIntegerField()
|
||||
dispatch_count = models.IntegerField(default=0)
|
||||
dispatch_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
settle_count = models.IntegerField(default=0)
|
||||
settle_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
refund_count = models.IntegerField(default=0)
|
||||
refund_amount = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00'))
|
||||
cancel_count = models.IntegerField(default=0)
|
||||
penalty_count = models.IntegerField(default=0)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_ri_tongji'
|
||||
unique_together = [['merchant_id', 'staff_user_id', 'stat_date']]
|
||||
indexes = [
|
||||
models.Index(fields=['merchant_id', 'stat_date', '-dispatch_count']),
|
||||
]
|
||||
|
||||
|
||||
class MerchantOrderDispatch(QModel):
|
||||
"""订单派单归属(不修改 orders 表)"""
|
||||
order_id = models.CharField(max_length=32, unique=True, db_index=True)
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
dispatch_actor_type = models.CharField(max_length=16, verbose_name='MERCHANT/STAFF')
|
||||
dispatch_actor_id = models.CharField(max_length=7)
|
||||
staff_member_id = models.BigIntegerField(null=True, blank=True, db_index=True)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_order_dispatch'
|
||||
|
||||
|
||||
class MerchantStaffAuditLog(QModel):
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
operator_user_id = models.CharField(max_length=7, db_index=True)
|
||||
operator_type = models.CharField(max_length=16, verbose_name='MERCHANT_OWNER/STAFF')
|
||||
staff_member_id = models.BigIntegerField(null=True, blank=True)
|
||||
action = models.CharField(max_length=64, db_index=True)
|
||||
resource_type = models.CharField(max_length=32, blank=True, default='')
|
||||
resource_id = models.CharField(max_length=64, blank=True, default='')
|
||||
value_before = models.JSONField(null=True, blank=True)
|
||||
value_after = models.JSONField(null=True, blank=True)
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True)
|
||||
request_ip = models.CharField(max_length=64, blank=True, default='')
|
||||
remark = models.CharField(max_length=500, blank=True, default='')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_staff_audit_log'
|
||||
indexes = [models.Index(fields=['merchant_id', '-CreateTime'])]
|
||||
|
||||
|
||||
class MerchantDispatchTemplate(QModel):
|
||||
merchant_id = models.CharField(max_length=7, db_index=True)
|
||||
template_name = models.CharField(max_length=128)
|
||||
template_type = models.CharField(max_length=16, default='EXPRESS')
|
||||
payload_json = models.JSONField(default=dict)
|
||||
created_by = models.CharField(max_length=7)
|
||||
created_by_type = models.CharField(max_length=16, default='MERCHANT')
|
||||
status = models.IntegerField(default=1)
|
||||
use_count = models.IntegerField(default=0)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'merchant_dispatch_template'
|
||||
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='派单扣额度',
|
||||
)
|
||||
31
merchant_ops/sql/seed_permissions.sql
Normal file
31
merchant_ops/sql/seed_permissions.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- 商家子客服:权限码初始化(部署后执行一次,或使用 python manage.py seed_merchant_staff)
|
||||
-- 表 merchant_staff_permission
|
||||
|
||||
INSERT INTO merchant_staff_permission (perm_code, perm_name, sort_order, status, CreateTime, UpdateTime) VALUES
|
||||
('staff_manage', '子客服管理', 0, 1, NOW(), NOW()),
|
||||
('role_manage', '角色管理', 1, 1, NOW(), NOW()),
|
||||
('wallet_allocate', '划额度', 2, 1, NOW(), NOW()),
|
||||
('wallet_view', '查看额度', 3, 1, NOW(), NOW()),
|
||||
('finance_view', '查看商家余额', 4, 1, NOW(), NOW()),
|
||||
('order_dispatch', '派单发单', 5, 1, NOW(), NOW()),
|
||||
('order_view_all', '查看全部订单', 6, 1, NOW(), NOW()),
|
||||
('order_view_self', '只看自己派的单', 7, 1, NOW(), NOW()),
|
||||
('order_detail', '订单详情', 8, 1, NOW(), NOW()),
|
||||
('order_settle', '结单', 9, 1, NOW(), NOW()),
|
||||
('order_refund_apply', '申请退款', 10, 1, NOW(), NOW()),
|
||||
('order_cancel', '撤销订单', 11, 1, NOW(), NOW()),
|
||||
('order_change_player', '换打手', 12, 1, NOW(), NOW()),
|
||||
('penalty_apply', '申请罚单', 13, 1, NOW(), NOW()),
|
||||
('penalty_manage', '改撤罚单', 14, 1, NOW(), NOW()),
|
||||
('penalty_view', '查看罚单', 15, 1, NOW(), NOW()),
|
||||
('dashou_ban', '封禁解封打手', 16, 1, NOW(), NOW()),
|
||||
('template_manage', '派单模板', 17, 1, NOW(), NOW()),
|
||||
('audit_view', '操作日志', 18, 1, NOW(), NOW()),
|
||||
('rank_view', '客服排行榜', 19, 1, NOW(), NOW()),
|
||||
('im_chat', '订单IM', 20, 1, NOW(), NOW()),
|
||||
('stats_view', '统计数据', 21, 1, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE perm_name=VALUES(perm_name), sort_order=VALUES(sort_order);
|
||||
|
||||
-- 推荐:执行完 SQL 后运行
|
||||
-- python manage.py seed_merchant_staff
|
||||
-- 会自动创建系统角色模板及角色-权限关联
|
||||
46
merchant_ops/urls.py
Normal file
46
merchant_ops/urls.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from django.urls import path
|
||||
|
||||
from merchant_ops.views_manage import (
|
||||
StaffInviteCreateView, StaffInviteListView, StaffInviteRevokeView,
|
||||
StaffMemberListView, StaffMemberUpdateRoleView, StaffMemberRemoveView,
|
||||
StaffMemberDisableView, StaffRoleListView, StaffRoleSetPermsView,
|
||||
StaffWalletAllocateView, StaffWalletLedgerView, StaffAuditListView,
|
||||
StaffRankView,
|
||||
)
|
||||
from merchant_ops.views_staff import StaffBindView, StaffMeView
|
||||
from merchant_ops.views_orders import (
|
||||
StaffOrderListView, StaffOrderDetailView, StaffDispatchView,
|
||||
StaffProductTypesView, StaffSettleView, StaffCancelView, StaffRefundView,
|
||||
StaffPenaltyApplyView, StaffPenaltyManageView, StaffChangePlayerView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
# 子客服
|
||||
path('bind', StaffBindView.as_view(), name='staff_bind'),
|
||||
path('me', StaffMeView.as_view(), name='staff_me'),
|
||||
# 老板管理
|
||||
path('invite/create', StaffInviteCreateView.as_view()),
|
||||
path('invite/list', StaffInviteListView.as_view()),
|
||||
path('invite/revoke', StaffInviteRevokeView.as_view()),
|
||||
path('member/list', StaffMemberListView.as_view()),
|
||||
path('member/update-role', StaffMemberUpdateRoleView.as_view()),
|
||||
path('member/remove', StaffMemberRemoveView.as_view()),
|
||||
path('member/disable', StaffMemberDisableView.as_view()),
|
||||
path('role/list', StaffRoleListView.as_view()),
|
||||
path('role/set-perms', StaffRoleSetPermsView.as_view()),
|
||||
path('wallet/allocate', StaffWalletAllocateView.as_view()),
|
||||
path('wallet/ledger', StaffWalletLedgerView.as_view()),
|
||||
path('audit/list', StaffAuditListView.as_view()),
|
||||
path('rank', StaffRankView.as_view()),
|
||||
# 子客服订单(全新接口)
|
||||
path('order/list', StaffOrderListView.as_view()),
|
||||
path('order/detail', StaffOrderDetailView.as_view()),
|
||||
path('order/dispatch', StaffDispatchView.as_view()),
|
||||
path('order/product-types', StaffProductTypesView.as_view()),
|
||||
path('order/settle', StaffSettleView.as_view()),
|
||||
path('order/cancel', StaffCancelView.as_view()),
|
||||
path('order/refund', StaffRefundView.as_view()),
|
||||
path('order/penalty-apply', StaffPenaltyApplyView.as_view()),
|
||||
path('order/penalty-manage', StaffPenaltyManageView.as_view()),
|
||||
path('order/change-player', StaffChangePlayerView.as_view()),
|
||||
]
|
||||
405
merchant_ops/views_manage.py
Normal file
405
merchant_ops/views_manage.py
Normal file
@@ -0,0 +1,405 @@
|
||||
"""商家老板:邀请码、成员、角色、额度、日志、排行。"""
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db.models import Sum, Q
|
||||
from django.utils import timezone
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from merchant_ops.constants import (
|
||||
INVITE_STATUS_REVOKED, INVITE_STATUS_UNUSED, INVITE_STATUS_USED,
|
||||
MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED, MEMBER_STATUS_REMOVED,
|
||||
)
|
||||
from merchant_ops.models import (
|
||||
MerchantStaffInvite, MerchantStaffMember, MerchantStaffRole,
|
||||
MerchantStaffRolePermission, MerchantStaffMemberPermission,
|
||||
MerchantStaffWallet, MerchantStaffWalletLedger, MerchantStaffAuditLog,
|
||||
MerchantStaffDailyStats, MerchantStaffPermission,
|
||||
)
|
||||
from merchant_ops.services.authz import (
|
||||
StaffAuthError, ensure_owner_merchant, get_member_permissions,
|
||||
is_merchant_owner, resolve_staff_context,
|
||||
get_active_staff_member, member_has_perm as _member_has_perm,
|
||||
)
|
||||
from merchant_ops.services.audit import audit_owner
|
||||
from merchant_ops.services.bootstrap import ensure_merchant_roles
|
||||
from merchant_ops.services.wallet import allocate_quota, get_or_create_wallet
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _err(e):
|
||||
if isinstance(e, StaffAuthError):
|
||||
return Response({'code': e.code, 'msg': e.msg, 'data': None})
|
||||
logger.exception(e)
|
||||
return Response({'code': 500, 'msg': '服务器错误', 'data': None})
|
||||
|
||||
|
||||
def _role_prefix(role_code):
|
||||
m = {'CHIEF': 'ZG', 'FINANCE': 'CW', 'DISPATCHER': 'PD'}
|
||||
return m.get(role_code, 'KF')
|
||||
|
||||
|
||||
class StaffInviteCreateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
role_id = request.data.get('role_id')
|
||||
if not role_id:
|
||||
return Response({'code': 400, 'msg': '请选择角色', 'data': None})
|
||||
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
|
||||
prefix = _role_prefix(role.role_code)
|
||||
code = f"{prefix}-{secrets.token_hex(3).upper()}"
|
||||
invite = MerchantStaffInvite.objects.create(
|
||||
merchant_id=request.user.UserUID,
|
||||
invite_code=code,
|
||||
role=role,
|
||||
status=INVITE_STATUS_UNUSED,
|
||||
created_by=request.user.UserUID,
|
||||
expire_at=timezone.now() + timedelta(days=30),
|
||||
)
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'INVITE_CREATE', request,
|
||||
resource_type='invite', resource_id=code)
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'invite_code': code, 'id': invite.id}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffInviteListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
status_filter = request.data.get('status') # None=all, 0,1,2
|
||||
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
|
||||
if status_filter is not None:
|
||||
qs = qs.filter(status=int(status_filter))
|
||||
items = []
|
||||
for inv in qs.order_by('-CreateTime')[:200]:
|
||||
item = {
|
||||
'id': inv.id,
|
||||
'code': inv.invite_code,
|
||||
'typeName': inv.role.role_name if inv.role else '',
|
||||
'status': inv.status,
|
||||
'adddate': inv.CreateTime.strftime('%Y-%m-%d') if inv.CreateTime else '',
|
||||
'usedate': inv.used_at.strftime('%Y-%m-%d') if inv.used_at else '',
|
||||
'user': None,
|
||||
}
|
||||
if inv.used_by:
|
||||
from users.business_models import User
|
||||
u = User.query.filter(UserUID=inv.used_by).first()
|
||||
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
|
||||
items.append(item)
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffInviteRevokeView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
invite_id = request.data.get('id')
|
||||
inv = MerchantStaffInvite.query.get(id=invite_id, merchant_id=request.user.UserUID, status=INVITE_STATUS_UNUSED)
|
||||
inv.status = INVITE_STATUS_REVOKED
|
||||
inv.save(update_fields=['status'])
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'INVITE_REVOKE', request, resource_id=inv.invite_code)
|
||||
return Response({'code': 0, 'msg': '已作废', 'data': None})
|
||||
except MerchantStaffInvite.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '邀请码不存在或已使用', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffMemberListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
mid = request.user.UserUID
|
||||
ensure_owner_merchant(mid, request.user)
|
||||
members = MerchantStaffMember.query.filter(
|
||||
merchant_id=mid, status=MEMBER_STATUS_ACTIVE,
|
||||
).select_related('role')
|
||||
result = []
|
||||
for m in members:
|
||||
stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate(
|
||||
dispatch_count=Sum('dispatch_count'),
|
||||
settle_count=Sum('settle_count'),
|
||||
settle_amount=Sum('settle_amount'),
|
||||
)
|
||||
wallet = m.wallets.first()
|
||||
result.append({
|
||||
'id': m.id,
|
||||
'uid': m.staff_user_id,
|
||||
'nickname': m.display_name or m.staff_user_id,
|
||||
'isChief': m.role.role_code == 'CHIEF',
|
||||
'isFinance': m.role.role_code == 'FINANCE',
|
||||
'role_name': m.role.role_name,
|
||||
'orderCount': stats['dispatch_count'] or 0,
|
||||
'settleCount': stats['settle_count'] or 0,
|
||||
'settleAmount': str(stats['settle_amount'] or '0.00'),
|
||||
'quota_available': str(wallet.quota_available if wallet else 0),
|
||||
})
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'list': result}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffMemberUpdateRoleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
member_id = request.data.get('member_id')
|
||||
role_id = request.data.get('role_id')
|
||||
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE)
|
||||
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
|
||||
old = m.role_id
|
||||
m.role = role
|
||||
m.save(update_fields=['role'])
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'MEMBER_UPDATE_ROLE', request,
|
||||
resource_type='member', resource_id=member_id,
|
||||
value_before={'role_id': old}, value_after={'role_id': role_id})
|
||||
return Response({'code': 0, 'msg': '成功', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffMemberRemoveView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
member_id = request.data.get('member_id')
|
||||
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID)
|
||||
m.status = MEMBER_STATUS_REMOVED
|
||||
m.revoked_at = timezone.now()
|
||||
m.save(update_fields=['status', 'revoked_at'])
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'MEMBER_REMOVE', request,
|
||||
resource_type='member', resource_id=member_id)
|
||||
return Response({'code': 0, 'msg': '已移除', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffMemberDisableView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
member_id = request.data.get('member_id')
|
||||
disable = request.data.get('disable', True)
|
||||
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID)
|
||||
m.status = MEMBER_STATUS_DISABLED if disable else MEMBER_STATUS_ACTIVE
|
||||
m.save(update_fields=['status'])
|
||||
action = 'MEMBER_DISABLE' if disable else 'MEMBER_ENABLE'
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, action, request, resource_id=member_id)
|
||||
return Response({'code': 0, 'msg': '成功', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffRoleListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
roles = MerchantStaffRole.query.filter(merchant_id=request.user.UserUID, status=1)
|
||||
data = []
|
||||
for r in roles:
|
||||
perms = list(r.role_permissions.values_list('perm_code', flat=True))
|
||||
data.append({
|
||||
'id': r.id, 'role_code': r.role_code, 'role_name': r.role_name,
|
||||
'is_system': r.is_system, 'permissions': perms,
|
||||
})
|
||||
all_perms = list(MerchantStaffPermission.query.filter(status=1).order_by('sort_order').values(
|
||||
'perm_code', 'perm_name'))
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'roles': data, 'all_permissions': all_perms}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffRoleSetPermsView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
role_id = request.data.get('role_id')
|
||||
permissions = request.data.get('permissions', [])
|
||||
role = MerchantStaffRole.query.get(id=role_id, merchant_id=request.user.UserUID)
|
||||
MerchantStaffRolePermission.query.filter(role=role).delete()
|
||||
for p in permissions:
|
||||
MerchantStaffRolePermission.objects.create(role=role, perm_code=p)
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'ROLE_SET_PERMS', request, resource_id=role_id)
|
||||
return Response({'code': 0, 'msg': '成功', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffWalletAllocateView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
ensure_owner_merchant(request.user.UserUID, request.user)
|
||||
member_id = request.data.get('member_id')
|
||||
amount = request.data.get('amount')
|
||||
m = MerchantStaffMember.query.get(id=member_id, merchant_id=request.user.UserUID, status=MEMBER_STATUS_ACTIVE)
|
||||
get_or_create_wallet(m)
|
||||
allocate_quota(m, amount, request.user.UserUID, remark='老板划额度')
|
||||
audit_owner(request.user.UserUID, request.user.UserUID, 'WALLET_ALLOCATE', request,
|
||||
resource_type='wallet', resource_id=member_id, amount=amount)
|
||||
return Response({'code': 0, 'msg': '划额度成功', 'data': None})
|
||||
except ValueError as e:
|
||||
return Response({'code': 400, 'msg': str(e), 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffWalletLedgerView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
mid = request.user.UserUID
|
||||
if is_merchant_owner(request.user):
|
||||
member_id = request.data.get('member_id')
|
||||
qs = MerchantStaffWalletLedger.query.filter(merchant_id=mid)
|
||||
if member_id:
|
||||
m = MerchantStaffMember.query.get(id=member_id, merchant_id=mid)
|
||||
qs = qs.filter(staff_user_id=m.staff_user_id)
|
||||
else:
|
||||
ctx = resolve_staff_context(request.user)
|
||||
if not ctx:
|
||||
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
||||
qs = MerchantStaffWalletLedger.query.filter(staff_user_id=request.user.UserUID)
|
||||
items = [{
|
||||
'change_type': x.change_type,
|
||||
'amount': str(x.amount),
|
||||
'balance_after': str(x.balance_after),
|
||||
'related_order_id': x.related_order_id or '',
|
||||
'remark': x.remark,
|
||||
'time': x.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if x.CreateTime else '',
|
||||
} for x in qs.order_by('-CreateTime')[:100]]
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'list': items}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffAuditListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
mid = request.user.UserUID
|
||||
member = None
|
||||
if is_merchant_owner(request.user):
|
||||
ensure_owner_merchant(mid, request.user)
|
||||
else:
|
||||
member = get_active_staff_member(request.user)
|
||||
if not member or not _member_has_perm(member, 'audit_view'):
|
||||
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
||||
mid = member.merchant_id
|
||||
page = max(1, int(request.data.get('page', 1)))
|
||||
page_size = min(50, int(request.data.get('page_size', 20)))
|
||||
qs = MerchantStaffAuditLog.query.filter(merchant_id=mid)
|
||||
staff_filter = request.data.get('staff_user_id')
|
||||
if staff_filter:
|
||||
qs = qs.filter(operator_user_id=staff_filter)
|
||||
total = qs.count()
|
||||
rows = qs.order_by('-CreateTime')[(page - 1) * page_size: page * page_size]
|
||||
items = [{
|
||||
'id': r.id,
|
||||
'operator_user_id': r.operator_user_id,
|
||||
'operator_type': r.operator_type,
|
||||
'action': r.action,
|
||||
'resource_type': r.resource_type,
|
||||
'resource_id': r.resource_id,
|
||||
'amount': str(r.amount) if r.amount is not None else '',
|
||||
'remark': r.remark,
|
||||
'time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||
} for r in rows]
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {
|
||||
'list': items, 'total': total, 'has_more': page * page_size < total,
|
||||
}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffRankView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
from datetime import date, timedelta
|
||||
from django.db.models import Sum
|
||||
|
||||
if is_merchant_owner(request.user):
|
||||
mid = request.user.UserUID
|
||||
else:
|
||||
member = get_active_staff_member(request.user)
|
||||
if not member:
|
||||
return Response({'code': 403, 'msg': '无权限', 'data': None})
|
||||
mid = member.merchant_id
|
||||
period = request.data.get('period', 'today')
|
||||
sort_by = request.data.get('sort_by', 'dispatch_count')
|
||||
today = date.today()
|
||||
if period == 'today':
|
||||
start = today
|
||||
elif period == 'yesterday':
|
||||
start = today - timedelta(days=1)
|
||||
elif period == 'week':
|
||||
start = today - timedelta(days=7)
|
||||
elif period == 'month':
|
||||
start = today - timedelta(days=30)
|
||||
else:
|
||||
start = None
|
||||
|
||||
qs = MerchantStaffDailyStats.query.filter(merchant_id=mid)
|
||||
if start:
|
||||
if period == 'yesterday':
|
||||
qs = qs.filter(stat_date=start)
|
||||
else:
|
||||
qs = qs.filter(stat_date__gte=start)
|
||||
|
||||
agg = qs.values('staff_user_id', 'member_id').annotate(
|
||||
dispatch_count=Sum('dispatch_count'),
|
||||
dispatch_amount=Sum('dispatch_amount'),
|
||||
settle_count=Sum('settle_count'),
|
||||
settle_amount=Sum('settle_amount'),
|
||||
)
|
||||
sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count'
|
||||
agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50]
|
||||
|
||||
result = []
|
||||
for idx, row in enumerate(agg, 1):
|
||||
m = MerchantStaffMember.query.filter(id=row['member_id']).first()
|
||||
result.append({
|
||||
'rank': idx,
|
||||
'member_id': row['member_id'],
|
||||
'staff_user_id': row['staff_user_id'],
|
||||
'display_name': (m.display_name if m else '') or row['staff_user_id'],
|
||||
'dispatch_count': row['dispatch_count'] or 0,
|
||||
'dispatch_amount': str(row['dispatch_amount'] or 0),
|
||||
'settle_count': row['settle_count'] or 0,
|
||||
'settle_amount': str(row['settle_amount'] or 0),
|
||||
})
|
||||
return Response({'code': 0, 'msg': '成功', 'data': {'list': result, 'period': period}})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
256
merchant_ops/views_orders.py
Normal file
256
merchant_ops/views_orders.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""子客服专用订单接口(不修改商家原接口)。"""
|
||||
import logging
|
||||
|
||||
from django.db.models import Q
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from backend.utils import pick_leixing_id, pick_order_id
|
||||
from merchant_ops.helpers import proxy_merchant_order_view, staff_can_access_order
|
||||
from merchant_ops.models import MerchantOrderDispatch
|
||||
from merchant_ops.services.authz import StaffAuthError, get_active_staff_member, member_has_perm
|
||||
from merchant_ops.services.audit import audit_staff
|
||||
from merchant_ops.services.dispatch import staff_dispatch_order
|
||||
from merchant_ops.services.stats import bump_cancel_count, bump_penalty_count, update_merchant_and_staff_stats
|
||||
from merchant_ops.constants import STAT_ACTION_REFUND, STAT_ACTION_SETTLE
|
||||
from orders.models import MerchantOrderExt
|
||||
from orders.views import (
|
||||
ShangjiaDingdanXiangqingView, ShangjiaJiesuanView, ShangjiaChexiaoView,
|
||||
ShangjiaTuikuanShenqingView, ShangjiaFakuanApplyView, ShangjiaFakuaiXiugaiView,
|
||||
ZxsjghdsView, ShangpinLeixingView,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _require_staff(request):
|
||||
member = get_active_staff_member(request.user)
|
||||
if not member:
|
||||
raise StaffAuthError(403, '非有效子客服')
|
||||
return member
|
||||
|
||||
|
||||
def _err(e):
|
||||
if isinstance(e, StaffAuthError):
|
||||
return Response({'code': e.code, 'msg': e.msg, 'data': None})
|
||||
logger.exception(e)
|
||||
return Response({'code': 500, 'msg': '服务器错误', 'data': None})
|
||||
|
||||
|
||||
class StaffOrderListView(APIView):
|
||||
"""POST /yonghu/merchant-staff/order/list — 对齐 sjdingdanhq"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
if not member_has_perm(member, 'order_view_all') and not member_has_perm(member, 'order_view_self'):
|
||||
raise StaffAuthError(403, '无订单查看权限')
|
||||
|
||||
data = request.data
|
||||
zhuangtai_list = data.get('zhuangtai_list', [])
|
||||
page = max(1, int(data.get('page', 1)))
|
||||
page_size = min(100, max(1, int(data.get('page_size', 10))))
|
||||
leixing_id = pick_leixing_id(data)
|
||||
keyword = str(data.get('keyword', '')).strip()
|
||||
|
||||
if not isinstance(zhuangtai_list, list) or not zhuangtai_list:
|
||||
return Response({'code': 3, 'msg': '状态列表不能为空', 'data': None})
|
||||
|
||||
base_qs = MerchantOrderExt.query.filter(
|
||||
MerchantID=member.merchant_id,
|
||||
).select_related('Order').filter(Order__Status__in=zhuangtai_list)
|
||||
|
||||
if not member_has_perm(member, 'order_view_all'):
|
||||
own_ids = MerchantOrderDispatch.query.filter(
|
||||
staff_member_id=member.id,
|
||||
).values_list('order_id', flat=True)
|
||||
base_qs = base_qs.filter(Order__OrderID__in=list(own_ids))
|
||||
|
||||
if leixing_id is not None:
|
||||
base_qs = base_qs.filter(Order__ProductTypeID=leixing_id)
|
||||
if keyword:
|
||||
base_qs = base_qs.filter(
|
||||
Q(Order__OrderID__exact=keyword)
|
||||
| Q(Order__Description__icontains=keyword)
|
||||
| Q(Order__Nickname__icontains=keyword)
|
||||
| Q(Order__PlayerID__exact=keyword)
|
||||
)
|
||||
|
||||
pending_count = MerchantOrderExt.query.filter(
|
||||
MerchantID=member.merchant_id, Order__Status=8,
|
||||
).count()
|
||||
total_count = base_qs.count()
|
||||
start = (page - 1) * page_size
|
||||
page_qs = base_qs.order_by('-Order__CreateTime')[start:start + page_size]
|
||||
|
||||
order_list = []
|
||||
for ext in page_qs:
|
||||
o = ext.Order
|
||||
order_list.append({
|
||||
'dingdan_id': o.OrderID,
|
||||
'zhuangtai': o.Status,
|
||||
'jine': float(o.Amount) if o.Amount else 0.0,
|
||||
'jieshao': o.Description or '',
|
||||
'beizhu': o.Remark or '',
|
||||
'nicheng': o.Nickname or '',
|
||||
'leixing_id': o.ProductTypeID,
|
||||
'tupian': o.ImageURL or '',
|
||||
'jiedan_dashou_id': o.PlayerID or '',
|
||||
'CreateTime': o.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if o.CreateTime else '',
|
||||
})
|
||||
|
||||
return Response({
|
||||
'code': 0, 'msg': '成功',
|
||||
'data': {
|
||||
'list': order_list,
|
||||
'has_more': len(page_qs) == page_size and start + page_size < total_count,
|
||||
'pending_count': pending_count,
|
||||
},
|
||||
})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffOrderDetailView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
if not member_has_perm(member, 'order_detail'):
|
||||
raise StaffAuthError(403, '无权限')
|
||||
resp = proxy_merchant_order_view(
|
||||
request, member, ShangjiaDingdanXiangqingView, 'order_detail',
|
||||
)
|
||||
if resp.status_code == 200 and resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'ORDER_DETAIL', request, resource_id=pick_order_id(request.data))
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffDispatchView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
if not member_has_perm(member, 'order_dispatch'):
|
||||
raise StaffAuthError(403, '无派单权限')
|
||||
ok, payload = staff_dispatch_order(member, request.user, request.data)
|
||||
if ok:
|
||||
audit_staff(member, 'DISPATCH', request,
|
||||
resource_id=payload.get('data', {}).get('dingdan_id'),
|
||||
amount=request.data.get('jiage'))
|
||||
code = payload.get('code', 500)
|
||||
return Response(payload, status=status.HTTP_200_OK if code in (0, 200) else status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffProductTypesView(APIView):
|
||||
"""商品类型 — 代理 sjspleixing"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
if not member_has_perm(member, 'order_dispatch'):
|
||||
raise StaffAuthError(403, '无权限')
|
||||
return proxy_merchant_order_view(request, member, ShangpinLeixingView, 'order_dispatch')
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffSettleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ShangjiaJiesuanView, 'order_settle')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'SETTLE', request, resource_id=pick_order_id(request.data))
|
||||
from orders.models import Order
|
||||
oid = pick_order_id(request.data)
|
||||
o = Order.query.filter(OrderID=oid).first()
|
||||
if o:
|
||||
update_merchant_and_staff_stats(member.merchant_id, member, o.Amount, STAT_ACTION_SETTLE)
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffCancelView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ShangjiaChexiaoView, 'order_cancel')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'CANCEL', request, resource_id=pick_order_id(request.data))
|
||||
bump_cancel_count(member.merchant_id, member)
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffRefundView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ShangjiaTuikuanShenqingView, 'order_refund_apply')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'REFUND_APPLY', request, resource_id=pick_order_id(request.data))
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffPenaltyApplyView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ShangjiaFakuanApplyView, 'penalty_apply')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'PENALTY_APPLY', request, resource_id=pick_order_id(request.data))
|
||||
bump_penalty_count(member.merchant_id, member)
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffPenaltyManageView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ShangjiaFakuaiXiugaiView, 'penalty_manage')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'PENALTY_MANAGE', request, resource_id=pick_order_id(request.data))
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffChangePlayerView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
member = _require_staff(request)
|
||||
resp = proxy_merchant_order_view(request, member, ZxsjghdsView, 'order_change_player')
|
||||
if resp.data.get('code') in (0, 200):
|
||||
audit_staff(member, 'CHANGE_PLAYER', request, resource_id=pick_order_id(request.data))
|
||||
return resp
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
107
merchant_ops/views_staff.py
Normal file
107
merchant_ops/views_staff.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""子客服:绑定、个人信息。"""
|
||||
import logging
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from merchant_ops.constants import INVITE_STATUS_UNUSED, INVITE_STATUS_USED, MEMBER_STATUS_ACTIVE
|
||||
from merchant_ops.models import MerchantStaffInvite, MerchantStaffMember
|
||||
from merchant_ops.services.authz import (
|
||||
StaffAuthError, assert_can_bind_staff, get_active_staff_member, resolve_staff_context,
|
||||
)
|
||||
from merchant_ops.services.audit import audit_staff
|
||||
from merchant_ops.services.wallet import get_or_create_wallet
|
||||
from users.business_models import User
|
||||
from users.models import UserShangjia
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _err(e):
|
||||
if isinstance(e, StaffAuthError):
|
||||
return Response({'code': e.code, 'msg': e.msg, 'data': None})
|
||||
logger.exception(e)
|
||||
return Response({'code': 500, 'msg': '服务器错误', 'data': None})
|
||||
|
||||
|
||||
class StaffBindView(APIView):
|
||||
"""POST /yonghu/merchant-staff/bind"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
assert_can_bind_staff(request.user)
|
||||
invite_code = str(request.data.get('inviteCode') or request.data.get('invite_code') or '').strip()
|
||||
if not invite_code or invite_code == 'XQKF2026':
|
||||
return Response({'code': 400, 'msg': '请使用商家提供的一次性邀请码', 'data': None})
|
||||
|
||||
with transaction.atomic():
|
||||
inv = MerchantStaffInvite.objects.select_for_update().get(
|
||||
invite_code=invite_code, status=INVITE_STATUS_UNUSED,
|
||||
)
|
||||
if inv.expire_at and inv.expire_at < timezone.now():
|
||||
inv.status = 3
|
||||
inv.save(update_fields=['status'])
|
||||
return Response({'code': 400, 'msg': '邀请码已过期', 'data': None})
|
||||
|
||||
if not inv.role_id:
|
||||
return Response({'code': 400, 'msg': '邀请码未绑定角色', 'data': None})
|
||||
|
||||
display = getattr(request.user, 'Nickname', '') or request.user.UserUID
|
||||
member, created = MerchantStaffMember.objects.get_or_create(
|
||||
merchant_id=inv.merchant_id,
|
||||
staff_user_id=request.user.UserUID,
|
||||
defaults={
|
||||
'role_id': inv.role_id,
|
||||
'status': MEMBER_STATUS_ACTIVE,
|
||||
'display_name': display[:64],
|
||||
'invited_by_code': inv,
|
||||
},
|
||||
)
|
||||
if not created:
|
||||
if member.status == MEMBER_STATUS_ACTIVE:
|
||||
return Response({'code': 400, 'msg': '您已是该商家客服', 'data': None})
|
||||
member.status = MEMBER_STATUS_ACTIVE
|
||||
member.role_id = inv.role_id
|
||||
member.display_name = display[:64]
|
||||
member.invited_by_code = inv
|
||||
member.revoked_at = None
|
||||
member.save()
|
||||
|
||||
inv.status = INVITE_STATUS_USED
|
||||
inv.used_by = request.user.UserUID
|
||||
inv.used_at = timezone.now()
|
||||
inv.save(update_fields=['status', 'used_by', 'used_at'])
|
||||
|
||||
get_or_create_wallet(member)
|
||||
|
||||
ctx = resolve_staff_context(request.user)
|
||||
shangjia = UserShangjia.query.filter(user__UserUID=inv.merchant_id).first()
|
||||
if ctx and shangjia:
|
||||
ctx['merchant_display'] = {'nicheng': shangjia.nicheng, 'merchant_id': inv.merchant_id}
|
||||
audit_staff(member, 'STAFF_BIND', request, resource_id=invite_code)
|
||||
return Response({'code': 0, 'msg': '绑定成功', 'data': {'staff_context': ctx}})
|
||||
except MerchantStaffInvite.DoesNotExist:
|
||||
return Response({'code': 404, 'msg': '邀请码无效或已使用', 'data': None})
|
||||
except Exception as e:
|
||||
return _err(e)
|
||||
|
||||
|
||||
class StaffMeView(APIView):
|
||||
"""GET/POST /yonghu/merchant-staff/me"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
ctx = resolve_staff_context(request.user)
|
||||
if not ctx:
|
||||
return Response({'code': 404, 'msg': '非子客服身份', 'data': None})
|
||||
member = get_active_staff_member(request.user)
|
||||
shangjia = UserShangjia.query.filter(user__UserUID=member.merchant_id).first()
|
||||
ctx['merchant_display'] = {
|
||||
'nicheng': shangjia.nicheng if shangjia else '',
|
||||
'sjyue': str(shangjia.yue) if shangjia else '0',
|
||||
}
|
||||
return Response({'code': 0, 'msg': '成功', 'data': ctx})
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAPIView, \
|
||||
ZaixianZhuangtaiAPIView, GuanshiXinxiView, ShangJiaXinXiView, BossBiaoshiView, YaoqingmaView, \
|
||||
@@ -157,10 +157,5 @@ urlpatterns = [
|
||||
path('tixianqr', TixianQueRenAutoView.as_view(), name='自动提现确认'),
|
||||
path('tixiancallback', csrf_exempt(TixianCallbackV3View.as_view()), name='提现回调'),
|
||||
path('phbhqsj', PhbHqsjView.as_view(), name='排行榜数据'),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
path('merchant-staff/', include('merchant_ops.urls')),
|
||||
]
|
||||
@@ -4899,6 +4899,16 @@ class ShangjiaZhuceView(APIView):
|
||||
# 用户不是商家,继续注册流程
|
||||
pass
|
||||
|
||||
# 2.5 子客服身份互斥:有效客服不可认证商家
|
||||
from merchant_ops.services.authz import assert_no_active_staff_when_merchant_register
|
||||
try:
|
||||
assert_no_active_staff_when_merchant_register(current_user)
|
||||
except Exception as e:
|
||||
from merchant_ops.services.authz import StaffAuthError
|
||||
if isinstance(e, StaffAuthError):
|
||||
return Response({'code': 403, 'message': e.msg, 'data': None}, status=status.HTTP_403_FORBIDDEN)
|
||||
raise
|
||||
|
||||
# 3. 根据邀请码查找对应的管理员
|
||||
try:
|
||||
# 从管理员表查找邀请码
|
||||
|
||||
Reference in New Issue
Block a user