feat: 角色协议(勾选+阅读秒数+卡点)与冻结说明按身份返回
新增协议表/签署留痕、后台与小程序 API;去掉手写依赖;支持抢单/注册/提现等卡点配置。冻结问号文案按角色字典下发。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -68,6 +68,12 @@ EXTRA_PERMISSIONS = [
|
|||||||
'查看罚款率/投诉率等解冻相关指标',
|
'查看罚款率/投诉率等解冻相关指标',
|
||||||
'资金冻结',
|
'资金冻结',
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'xycfg001',
|
||||||
|
'角色协议配置',
|
||||||
|
'配置打手/管事/组长用户协议并发布版本',
|
||||||
|
'小程序配置',
|
||||||
|
),
|
||||||
# ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ----------
|
# ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ----------
|
||||||
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
|
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
|
||||||
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),
|
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),
|
||||||
|
|||||||
@@ -42,3 +42,7 @@ class Command(BaseCommand):
|
|||||||
call_command('seed_fund_freeze_perm')
|
call_command('seed_fund_freeze_perm')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.stdout.write(self.style.WARNING(f'seed_fund_freeze_perm 跳过: {e}'))
|
self.stdout.write(self.style.WARNING(f'seed_fund_freeze_perm 跳过: {e}'))
|
||||||
|
try:
|
||||||
|
call_command('seed_role_agreement_perm')
|
||||||
|
except Exception as e:
|
||||||
|
self.stdout.write(self.style.WARNING(f'seed_role_agreement_perm 跳过: {e}'))
|
||||||
|
|||||||
119
jituan/management/commands/seed_role_agreement_perm.py
Normal file
119
jituan/management/commands/seed_role_agreement_perm.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
"""写入角色协议权限码,并可按俱乐部挂到角色。
|
||||||
|
|
||||||
|
权限码:
|
||||||
|
xycfg001 角色协议配置
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python manage.py seed_role_agreement_perm
|
||||||
|
python manage.py seed_role_agreement_perm --club-id lxs --role-name 管理员
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from gvsdsdk.models import Permission, Role, RolePermission
|
||||||
|
|
||||||
|
from jituan.models import Club
|
||||||
|
|
||||||
|
PERMS = [
|
||||||
|
('xycfg001', '角色协议配置', '配置打手/管事/组长用户协议并发布版本', '小程序配置'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '写入角色协议权限码,并按俱乐部挂到角色'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--club-id', default='', help='仅处理该俱乐部')
|
||||||
|
parser.add_argument('--all-clubs', action='store_true')
|
||||||
|
parser.add_argument('--role-name', default='', help='角色名;空则只确保权限码')
|
||||||
|
parser.add_argument('--dry-run', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
dry_run = options['dry_run']
|
||||||
|
club_id = (options.get('club_id') or '').strip()
|
||||||
|
all_clubs = options.get('all_clubs')
|
||||||
|
role_name = (options.get('role_name') or '').strip()
|
||||||
|
|
||||||
|
sample = Permission.objects.filter(PermStatus=1).first()
|
||||||
|
tenant_uuid = sample.TenantUUID if sample else uuid.UUID(
|
||||||
|
'00000000-0000-0000-0000-000000000001'
|
||||||
|
).bytes
|
||||||
|
|
||||||
|
perm_objs = []
|
||||||
|
for code, name, desc, cat in PERMS:
|
||||||
|
perm = Permission.objects.filter(PermCode=code).first()
|
||||||
|
if perm:
|
||||||
|
self.stdout.write(f' 权限码已存在: {code}')
|
||||||
|
if not dry_run:
|
||||||
|
changed = False
|
||||||
|
if perm.PermName != name:
|
||||||
|
perm.PermName = name
|
||||||
|
changed = True
|
||||||
|
if (perm.PermDesc or '') != desc:
|
||||||
|
perm.PermDesc = desc
|
||||||
|
changed = True
|
||||||
|
if (perm.PermCategory or '') != cat:
|
||||||
|
perm.PermCategory = cat
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
perm.save(update_fields=['PermName', 'PermDesc', 'PermCategory'])
|
||||||
|
else:
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(f'would create perm {code}')
|
||||||
|
else:
|
||||||
|
perm = Permission.objects.create(
|
||||||
|
PermUUID=uuid.uuid4().bytes,
|
||||||
|
TenantUUID=tenant_uuid,
|
||||||
|
PermCode=code,
|
||||||
|
PermName=name,
|
||||||
|
PermDesc=desc,
|
||||||
|
PermCategory=cat,
|
||||||
|
PermStatus=1,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
UpdateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(f' 已创建权限码: {code}'))
|
||||||
|
if perm:
|
||||||
|
perm_objs.append(perm)
|
||||||
|
|
||||||
|
if not role_name:
|
||||||
|
self.stdout.write('未指定 --role-name,跳过挂角色')
|
||||||
|
return
|
||||||
|
|
||||||
|
clubs = []
|
||||||
|
if all_clubs:
|
||||||
|
clubs = list(Club.query.filter(status=1).values_list('club_id', flat=True))
|
||||||
|
elif club_id:
|
||||||
|
clubs = [club_id]
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING('请指定 --club-id 或 --all-clubs'))
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
for cid in clubs:
|
||||||
|
roles = Role.objects.filter(RoleName=role_name, ClubID=cid, RoleStatus=1)
|
||||||
|
if not roles.exists():
|
||||||
|
roles = Role.objects.filter(RoleName=role_name, RoleStatus=1)
|
||||||
|
roles = [r for r in roles if (getattr(r, 'ClubID', None) or '') == cid]
|
||||||
|
for role in roles:
|
||||||
|
for perm in perm_objs:
|
||||||
|
exists = RolePermission.objects.filter(
|
||||||
|
RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID,
|
||||||
|
).exists()
|
||||||
|
if exists:
|
||||||
|
self.stdout.write(f' 已挂: {cid} {role_name} <- {perm.PermCode}')
|
||||||
|
continue
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(f'would link {cid} {role_name} <- {perm.PermCode}')
|
||||||
|
else:
|
||||||
|
RolePermission.objects.create(
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
PermUUID=perm.PermUUID,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 已挂: {cid} {role_name} <- {perm.PermCode}'
|
||||||
|
))
|
||||||
66
jituan/migrations/0018_role_agreement.py
Normal file
66
jituan/migrations/0018_role_agreement.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Generated manually for role agreement tables
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0017_fund_freeze'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubRoleAgreement',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, max_length=16)),
|
||||||
|
('role', models.CharField(choices=[('dashou', '打手'), ('guanshi', '管事'), ('zuzhang', '组长')], db_index=True, max_length=16)),
|
||||||
|
('enabled', models.BooleanField(default=False, verbose_name='是否要求签署')),
|
||||||
|
('title', models.CharField(blank=True, default='', max_length=128, verbose_name='协议标题')),
|
||||||
|
('content_text', models.TextField(blank=True, default='', verbose_name='协议正文')),
|
||||||
|
('images_json', models.JSONField(blank=True, default=list, verbose_name='说明图片URL列表')),
|
||||||
|
('version', models.PositiveIntegerField(default=0, verbose_name='已发布版本号')),
|
||||||
|
('published_at', models.DateTimeField(blank=True, null=True)),
|
||||||
|
('updated_by', models.CharField(blank=True, default='', max_length=32)),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '角色协议',
|
||||||
|
'db_table': 'club_role_agreement',
|
||||||
|
'unique_together': {('club_id', 'role')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubRoleAgreementSign',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, max_length=16)),
|
||||||
|
('user_id', models.CharField(db_index=True, max_length=16, verbose_name='UserUID')),
|
||||||
|
('role', models.CharField(db_index=True, max_length=16)),
|
||||||
|
('agreement_id', models.BigIntegerField(db_index=True, default=0)),
|
||||||
|
('version', models.PositiveIntegerField(db_index=True)),
|
||||||
|
('title_snapshot', models.CharField(blank=True, default='', max_length=128)),
|
||||||
|
('content_snapshot', models.TextField(blank=True, default='')),
|
||||||
|
('images_snapshot', models.JSONField(blank=True, default=list)),
|
||||||
|
('sign_image_url', models.CharField(blank=True, default='', max_length=512)),
|
||||||
|
('client_meta', models.JSONField(blank=True, default=dict)),
|
||||||
|
('signed_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '角色协议签署记录',
|
||||||
|
'db_table': 'club_role_agreement_sign',
|
||||||
|
'unique_together': {('club_id', 'user_id', 'role', 'version')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clubroleagreementsign',
|
||||||
|
index=models.Index(fields=['user_id', 'role', 'version'], name='idx_cras_user_role_ver'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='clubroleagreementsign',
|
||||||
|
index=models.Index(fields=['club_id', 'role', 'signed_at'], name='idx_cras_club_role_t'),
|
||||||
|
),
|
||||||
|
]
|
||||||
22
jituan/migrations/0019_role_agreement_gates.py
Normal file
22
jituan/migrations/0019_role_agreement_gates.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 角色协议:阅读秒数 + 卡点配置(去掉手写依赖,字段兼容保留)
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('jituan', '0018_role_agreement'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubroleagreement',
|
||||||
|
name='read_seconds',
|
||||||
|
field=models.PositiveIntegerField(default=2, verbose_name='阅读秒数'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='clubroleagreement',
|
||||||
|
name='gate_config',
|
||||||
|
field=models.JSONField(blank=True, default=dict, verbose_name='签署卡点配置'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -615,3 +615,60 @@ class FundRiskStatDaily(QModel):
|
|||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['club_id', 'role', 'day'], name='idx_frsd_club_role_day'),
|
models.Index(fields=['club_id', 'role', 'day'], name='idx_frsd_club_role_day'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ClubRoleAgreement(QModel):
|
||||||
|
"""俱乐部×身份用户协议(当前生效配置,发布时 version+1)。"""
|
||||||
|
ROLE_CHOICES = (
|
||||||
|
('dashou', '打手'),
|
||||||
|
('guanshi', '管事'),
|
||||||
|
('zuzhang', '组长'),
|
||||||
|
)
|
||||||
|
|
||||||
|
club_id = models.CharField(max_length=16, db_index=True)
|
||||||
|
role = models.CharField(max_length=16, choices=ROLE_CHOICES, db_index=True)
|
||||||
|
enabled = models.BooleanField(default=False, verbose_name='是否要求签署')
|
||||||
|
title = models.CharField(max_length=128, blank=True, default='', verbose_name='协议标题')
|
||||||
|
content_text = models.TextField(blank=True, default='', verbose_name='协议正文')
|
||||||
|
images_json = models.JSONField(default=list, blank=True, verbose_name='说明图片URL列表')
|
||||||
|
# 勾选前强制停留秒数,0=可立即勾选
|
||||||
|
read_seconds = models.PositiveIntegerField(default=2, verbose_name='阅读秒数')
|
||||||
|
# 卡点开关:qiangdan/register/tixian/invite,按角色默认见服务层
|
||||||
|
gate_config = models.JSONField(default=dict, blank=True, verbose_name='签署卡点配置')
|
||||||
|
version = models.PositiveIntegerField(default=0, verbose_name='已发布版本号')
|
||||||
|
published_at = models.DateTimeField(null=True, blank=True)
|
||||||
|
updated_by = models.CharField(max_length=32, blank=True, default='')
|
||||||
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
|
UpdateTime = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'club_role_agreement'
|
||||||
|
verbose_name = '角色协议'
|
||||||
|
unique_together = [['club_id', 'role']]
|
||||||
|
|
||||||
|
|
||||||
|
class ClubRoleAgreementSign(QModel):
|
||||||
|
"""用户签署某俱乐部某身份某版本协议的留痕(勾选确认,无手写图)。"""
|
||||||
|
club_id = models.CharField(max_length=16, db_index=True)
|
||||||
|
user_id = models.CharField(max_length=16, db_index=True, verbose_name='UserUID')
|
||||||
|
role = models.CharField(max_length=16, db_index=True)
|
||||||
|
agreement_id = models.BigIntegerField(db_index=True, default=0)
|
||||||
|
version = models.PositiveIntegerField(db_index=True)
|
||||||
|
title_snapshot = models.CharField(max_length=128, blank=True, default='')
|
||||||
|
content_snapshot = models.TextField(blank=True, default='')
|
||||||
|
images_snapshot = models.JSONField(default=list, blank=True)
|
||||||
|
# 兼容旧字段,新签署固定留空
|
||||||
|
sign_image_url = models.CharField(max_length=512, blank=True, default='')
|
||||||
|
client_meta = models.JSONField(default=dict, blank=True)
|
||||||
|
signed_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||||
|
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||||
|
UpdateTime = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'club_role_agreement_sign'
|
||||||
|
verbose_name = '角色协议签署记录'
|
||||||
|
unique_together = [['club_id', 'user_id', 'role', 'version']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['user_id', 'role', 'version'], name='idx_cras_user_role_ver'),
|
||||||
|
models.Index(fields=['club_id', 'role', 'signed_at'], name='idx_cras_club_role_t'),
|
||||||
|
]
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ KEFU_MENU_ROW_DEFS = [
|
|||||||
'perm_codes': ['phbj666', '8080a']},
|
'perm_codes': ['phbj666', '8080a']},
|
||||||
{'page_id': 'miniapp.recharge-copy', 'name': '充值页文案', 'path': '/miniapp/recharge-copy', 'parent_id': 'miniapp', 'sort_order': 119,
|
{'page_id': 'miniapp.recharge-copy', 'name': '充值页文案', 'path': '/miniapp/recharge-copy', 'parent_id': 'miniapp', 'sort_order': 119,
|
||||||
'perm_codes': ['8080a']},
|
'perm_codes': ['8080a']},
|
||||||
|
{'page_id': 'miniapp.role-agreement', 'name': '角色协议', 'path': '/miniapp/role-agreement', 'parent_id': 'miniapp', 'sort_order': 120,
|
||||||
|
'perm_codes': ['xycfg001', '8080a']},
|
||||||
{'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120},
|
{'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120},
|
||||||
{'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121,
|
{'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121,
|
||||||
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
|
'perm_codes': ['999a', '999b', '999c', '999d', '999e']},
|
||||||
|
|||||||
355
jituan/services/role_agreement.py
Normal file
355
jituan/services/role_agreement.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
"""角色协议:后台配置 + 小程序勾选签署(按俱乐部隔离)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from jituan.constants import CLUB_ID_DEFAULT
|
||||||
|
from jituan.models import ClubRoleAgreement, ClubRoleAgreementSign
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
|
||||||
|
ROLES = (
|
||||||
|
('dashou', '打手/接单员'),
|
||||||
|
('guanshi', '管事'),
|
||||||
|
('zuzhang', '组长'),
|
||||||
|
)
|
||||||
|
|
||||||
|
ROLE_SET = {r[0] for r in ROLES}
|
||||||
|
|
||||||
|
# 卡点:可后台逐项开关;默认贴近产品约定
|
||||||
|
GATE_DEFS = (
|
||||||
|
('qiangdan', '抢单/接单时'),
|
||||||
|
('register', '身份注册时'),
|
||||||
|
('tixian', '提现时'),
|
||||||
|
('invite', '邀请下级时'),
|
||||||
|
)
|
||||||
|
GATE_SET = {g[0] for g in GATE_DEFS}
|
||||||
|
|
||||||
|
DEFAULT_GATES = {
|
||||||
|
'dashou': {'qiangdan': True, 'register': False, 'tixian': False, 'invite': False},
|
||||||
|
'guanshi': {'qiangdan': False, 'register': True, 'tixian': True, 'invite': False},
|
||||||
|
'zuzhang': {'qiangdan': False, 'register': False, 'tixian': True, 'invite': False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _club(request) -> str:
|
||||||
|
return (resolve_club_id_from_request(request) or CLUB_ID_DEFAULT).strip() or CLUB_ID_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def _images(val) -> List[str]:
|
||||||
|
if not val:
|
||||||
|
return []
|
||||||
|
if isinstance(val, list):
|
||||||
|
return [str(x).strip() for x in val if str(x).strip()][:20]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp_read_seconds(val) -> int:
|
||||||
|
try:
|
||||||
|
n = int(val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
n = 2
|
||||||
|
if n < 0:
|
||||||
|
n = 0
|
||||||
|
if n > 300:
|
||||||
|
n = 300
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_gates(role: str, raw) -> Dict[str, bool]:
|
||||||
|
base = dict(DEFAULT_GATES.get(role) or {k: False for k in GATE_SET})
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
for k in GATE_SET:
|
||||||
|
if k in raw:
|
||||||
|
base[k] = bool(raw.get(k))
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def _row_to_dict(row: Optional[ClubRoleAgreement], club_id: str, role: str) -> Dict[str, Any]:
|
||||||
|
if not row:
|
||||||
|
return {
|
||||||
|
'club_id': club_id,
|
||||||
|
'role': role,
|
||||||
|
'enabled': False,
|
||||||
|
'title': '',
|
||||||
|
'content_text': '',
|
||||||
|
'images': [],
|
||||||
|
'read_seconds': 2,
|
||||||
|
'gate_config': normalize_gates(role, None),
|
||||||
|
'gate_defs': [{'key': k, 'label': lab} for k, lab in GATE_DEFS],
|
||||||
|
'version': 0,
|
||||||
|
'published_at': '',
|
||||||
|
'exists': False,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'club_id': row.club_id,
|
||||||
|
'role': row.role,
|
||||||
|
'enabled': bool(row.enabled),
|
||||||
|
'title': row.title or '',
|
||||||
|
'content_text': row.content_text or '',
|
||||||
|
'images': _images(row.images_json),
|
||||||
|
'read_seconds': _clamp_read_seconds(getattr(row, 'read_seconds', 2)),
|
||||||
|
'gate_config': normalize_gates(role, getattr(row, 'gate_config', None)),
|
||||||
|
'gate_defs': [{'key': k, 'label': lab} for k, lab in GATE_DEFS],
|
||||||
|
'version': int(row.version or 0),
|
||||||
|
'published_at': row.published_at.isoformat() if row.published_at else '',
|
||||||
|
'updated_by': row.updated_by or '',
|
||||||
|
'exists': True,
|
||||||
|
'id': row.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_admin_payload(request) -> Dict[str, Any]:
|
||||||
|
club_id = _club(request)
|
||||||
|
rows = {
|
||||||
|
r.role: r
|
||||||
|
for r in ClubRoleAgreement.query.filter(club_id=club_id)
|
||||||
|
}
|
||||||
|
items = []
|
||||||
|
for role, name in ROLES:
|
||||||
|
d = _row_to_dict(rows.get(role), club_id, role)
|
||||||
|
d['role_name'] = name
|
||||||
|
items.append(d)
|
||||||
|
return {
|
||||||
|
'club_id': club_id,
|
||||||
|
'roles': items,
|
||||||
|
'gate_defs': [{'key': k, 'label': lab} for k, lab in GATE_DEFS],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_agreement(request, data: dict, operator: str = '') -> Tuple[Optional[dict], Optional[str]]:
|
||||||
|
club_id = _club(request)
|
||||||
|
role = (data.get('role') or '').strip().lower()
|
||||||
|
if role not in ROLE_SET:
|
||||||
|
return None, '无效角色'
|
||||||
|
|
||||||
|
title = (data.get('title') or '')[:128]
|
||||||
|
content_text = data.get('content_text') or ''
|
||||||
|
if len(content_text) > 50000:
|
||||||
|
return None, '正文过长'
|
||||||
|
images = _images(data.get('images') or data.get('images_json'))
|
||||||
|
enabled = bool(data.get('enabled'))
|
||||||
|
read_seconds = _clamp_read_seconds(data.get('read_seconds', 2))
|
||||||
|
gate_config = normalize_gates(role, data.get('gate_config'))
|
||||||
|
|
||||||
|
row = ClubRoleAgreement.query.filter(club_id=club_id, role=role).first()
|
||||||
|
if not row:
|
||||||
|
row = ClubRoleAgreement(club_id=club_id, role=role, version=0)
|
||||||
|
row.enabled = enabled
|
||||||
|
row.title = title
|
||||||
|
row.content_text = content_text
|
||||||
|
row.images_json = images
|
||||||
|
row.read_seconds = read_seconds
|
||||||
|
row.gate_config = gate_config
|
||||||
|
row.updated_by = (operator or '')[:32]
|
||||||
|
row.save()
|
||||||
|
out = _row_to_dict(row, club_id, role)
|
||||||
|
out['role_name'] = dict(ROLES).get(role, role)
|
||||||
|
return out, None
|
||||||
|
|
||||||
|
|
||||||
|
def publish_agreement(request, data: dict, operator: str = '') -> Tuple[Optional[dict], Optional[str]]:
|
||||||
|
"""保存内容并 version+1,强制用户重签新版本。"""
|
||||||
|
club_id = _club(request)
|
||||||
|
role = (data.get('role') or '').strip().lower()
|
||||||
|
if role not in ROLE_SET:
|
||||||
|
return None, '无效角色'
|
||||||
|
|
||||||
|
saved, err = save_agreement(request, data, operator=operator)
|
||||||
|
if err:
|
||||||
|
return None, err
|
||||||
|
|
||||||
|
row = ClubRoleAgreement.query.filter(club_id=club_id, role=role).first()
|
||||||
|
if not row:
|
||||||
|
return None, '协议不存在'
|
||||||
|
if not (row.title or '').strip() and not (row.content_text or '').strip() and not _images(row.images_json):
|
||||||
|
return None, '请先填写标题、正文或上传图片再发布'
|
||||||
|
|
||||||
|
row.version = int(row.version or 0) + 1
|
||||||
|
row.published_at = timezone.now()
|
||||||
|
row.enabled = True if data.get('enabled') is None else bool(data.get('enabled'))
|
||||||
|
row.updated_by = (operator or '')[:32]
|
||||||
|
row.save()
|
||||||
|
out = _row_to_dict(row, club_id, role)
|
||||||
|
out['role_name'] = dict(ROLES).get(role, role)
|
||||||
|
return out, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_for_user(
|
||||||
|
club_id: str,
|
||||||
|
user_id: str,
|
||||||
|
role: str,
|
||||||
|
*,
|
||||||
|
gate: str = '',
|
||||||
|
view_only: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
取某角色当前协议。
|
||||||
|
- gate 非空:仅当该卡点开启且未签当前版时 need_sign=True
|
||||||
|
- view_only:只读查看,不强制签署
|
||||||
|
"""
|
||||||
|
role = (role or '').strip().lower()
|
||||||
|
if role not in ROLE_SET:
|
||||||
|
return {'need_sign': False, 'enabled': False, 'msg': '无效角色'}
|
||||||
|
|
||||||
|
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||||
|
row = ClubRoleAgreement.query.filter(club_id=cid, role=role).first()
|
||||||
|
empty = {
|
||||||
|
'need_sign': False,
|
||||||
|
'enabled': False,
|
||||||
|
'club_id': cid,
|
||||||
|
'role': role,
|
||||||
|
'version': 0,
|
||||||
|
'title': '',
|
||||||
|
'content_text': '',
|
||||||
|
'images': [],
|
||||||
|
'read_seconds': 0,
|
||||||
|
'gate_config': normalize_gates(role, None),
|
||||||
|
'gate_active': False,
|
||||||
|
'already_signed': False,
|
||||||
|
}
|
||||||
|
if not row or int(row.version or 0) <= 0:
|
||||||
|
return empty
|
||||||
|
|
||||||
|
version = int(row.version)
|
||||||
|
gates = normalize_gates(role, getattr(row, 'gate_config', None))
|
||||||
|
signed = ClubRoleAgreementSign.query.filter(
|
||||||
|
club_id=cid, user_id=str(user_id), role=role, version=version,
|
||||||
|
).exists()
|
||||||
|
enabled = bool(row.enabled)
|
||||||
|
read_seconds = _clamp_read_seconds(getattr(row, 'read_seconds', 2))
|
||||||
|
|
||||||
|
gate_key = (gate or '').strip().lower()
|
||||||
|
gate_active = True
|
||||||
|
if gate_key:
|
||||||
|
if gate_key not in GATE_SET:
|
||||||
|
return {**empty, 'msg': '无效卡点', 'enabled': enabled}
|
||||||
|
gate_active = bool(gates.get(gate_key))
|
||||||
|
|
||||||
|
need_sign = False
|
||||||
|
if enabled and not view_only and not signed and gate_active:
|
||||||
|
need_sign = True
|
||||||
|
# 无 gate 且非 view:兼容旧调用,只要启用且未签就 need_sign
|
||||||
|
if enabled and not view_only and not signed and not gate_key:
|
||||||
|
need_sign = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
'need_sign': need_sign,
|
||||||
|
'enabled': enabled,
|
||||||
|
'club_id': cid,
|
||||||
|
'role': role,
|
||||||
|
'role_name': dict(ROLES).get(role, role),
|
||||||
|
'version': version,
|
||||||
|
'title': row.title or '角色服务协议',
|
||||||
|
'content_text': row.content_text or '',
|
||||||
|
'images': _images(row.images_json),
|
||||||
|
'read_seconds': read_seconds,
|
||||||
|
'gate_config': gates,
|
||||||
|
'gate': gate_key,
|
||||||
|
'gate_active': gate_active if gate_key else enabled,
|
||||||
|
'agreement_id': row.id,
|
||||||
|
'published_at': row.published_at.isoformat() if row.published_at else '',
|
||||||
|
'already_signed': signed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def list_for_user(club_id: str, user_id: str, roles: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||||
|
"""小程序「我的协议」列表:按俱乐部返回已发布协议摘要。"""
|
||||||
|
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||||
|
want = [r for r in (roles or list(ROLE_SET)) if r in ROLE_SET]
|
||||||
|
if not want:
|
||||||
|
want = list(ROLE_SET)
|
||||||
|
items = []
|
||||||
|
for role in want:
|
||||||
|
d = get_current_for_user(cid, user_id, role, view_only=True)
|
||||||
|
if int(d.get('version') or 0) <= 0:
|
||||||
|
continue
|
||||||
|
items.append({
|
||||||
|
'role': d['role'],
|
||||||
|
'role_name': d.get('role_name') or role,
|
||||||
|
'title': d.get('title') or '',
|
||||||
|
'version': d.get('version') or 0,
|
||||||
|
'already_signed': bool(d.get('already_signed')),
|
||||||
|
'enabled': bool(d.get('enabled')),
|
||||||
|
'published_at': d.get('published_at') or '',
|
||||||
|
})
|
||||||
|
return {'club_id': cid, 'items': items}
|
||||||
|
|
||||||
|
|
||||||
|
def sign_agreement(
|
||||||
|
club_id: str,
|
||||||
|
user_id: str,
|
||||||
|
role: str,
|
||||||
|
*,
|
||||||
|
client_meta: Optional[dict] = None,
|
||||||
|
agreed: bool = False,
|
||||||
|
) -> Tuple[Optional[dict], Optional[str]]:
|
||||||
|
"""勾选确认签署;不再要求手写签名图。"""
|
||||||
|
role = (role or '').strip().lower()
|
||||||
|
if role not in ROLE_SET:
|
||||||
|
return None, '无效角色'
|
||||||
|
if not agreed:
|
||||||
|
return None, '请勾选同意协议'
|
||||||
|
|
||||||
|
cid = (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||||
|
row = ClubRoleAgreement.query.filter(club_id=cid, role=role).first()
|
||||||
|
if not row or not row.enabled or int(row.version or 0) <= 0:
|
||||||
|
return None, '当前无需签署或协议未发布'
|
||||||
|
|
||||||
|
version = int(row.version)
|
||||||
|
existed = ClubRoleAgreementSign.query.filter(
|
||||||
|
club_id=cid, user_id=str(user_id), role=role, version=version,
|
||||||
|
).first()
|
||||||
|
if existed:
|
||||||
|
return {
|
||||||
|
'already_signed': True,
|
||||||
|
'sign_id': existed.id,
|
||||||
|
'version': version,
|
||||||
|
'signed_at': existed.signed_at.isoformat() if existed.signed_at else '',
|
||||||
|
}, None
|
||||||
|
|
||||||
|
meta = dict(client_meta or {})
|
||||||
|
meta.setdefault('sign_mode', 'checkbox')
|
||||||
|
sign = ClubRoleAgreementSign.query.create(
|
||||||
|
club_id=cid,
|
||||||
|
user_id=str(user_id),
|
||||||
|
role=role,
|
||||||
|
agreement_id=row.id,
|
||||||
|
version=version,
|
||||||
|
title_snapshot=(row.title or '')[:128],
|
||||||
|
content_snapshot=row.content_text or '',
|
||||||
|
images_snapshot=_images(row.images_json),
|
||||||
|
sign_image_url='',
|
||||||
|
client_meta=meta,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'already_signed': False,
|
||||||
|
'sign_id': sign.id,
|
||||||
|
'version': version,
|
||||||
|
'signed_at': sign.signed_at.isoformat() if sign.signed_at else '',
|
||||||
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
def upload_agreement_image(file_obj, club_id: str, kind: str = 'content') -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""后台上传协议配图,返回相对路径。"""
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss
|
||||||
|
|
||||||
|
ok, err = validate_image(file_obj)
|
||||||
|
if not ok:
|
||||||
|
return None, err or '图片无效'
|
||||||
|
cid = (club_id or 'xq').strip() or 'xq'
|
||||||
|
kind = 'content'
|
||||||
|
ext = 'png'
|
||||||
|
name = getattr(file_obj, 'name', '') or ''
|
||||||
|
if '.' in name:
|
||||||
|
ext = name.rsplit('.', 1)[-1].lower()[:8] or 'png'
|
||||||
|
key = f'role_agreement/{cid}/{kind}/{uuid.uuid4().hex}.{ext}'
|
||||||
|
try:
|
||||||
|
path = upload_to_oss(file_obj, key)
|
||||||
|
if not path:
|
||||||
|
return None, '上传失败'
|
||||||
|
except Exception as e:
|
||||||
|
return None, f'上传失败: {e}'
|
||||||
|
return path, None
|
||||||
@@ -106,6 +106,7 @@ from jituan.views_fund_freeze import (
|
|||||||
FundFreezeLedgerView,
|
FundFreezeLedgerView,
|
||||||
FundFreezeRiskView,
|
FundFreezeRiskView,
|
||||||
)
|
)
|
||||||
|
from jituan.views_role_agreement import RoleAgreementAdminView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
|
path('auth/wechat-login', ClubWechatLoginView.as_view(), name='jituan_wechat_login'),
|
||||||
@@ -120,6 +121,7 @@ urlpatterns = [
|
|||||||
path('houtai/fund-freeze-config', FundFreezeConfigView.as_view(), name='jituan_fund_freeze_config'),
|
path('houtai/fund-freeze-config', FundFreezeConfigView.as_view(), name='jituan_fund_freeze_config'),
|
||||||
path('houtai/fund-freeze-ledger', FundFreezeLedgerView.as_view(), name='jituan_fund_freeze_ledger'),
|
path('houtai/fund-freeze-ledger', FundFreezeLedgerView.as_view(), name='jituan_fund_freeze_ledger'),
|
||||||
path('houtai/fund-freeze-risk', FundFreezeRiskView.as_view(), name='jituan_fund_freeze_risk'),
|
path('houtai/fund-freeze-risk', FundFreezeRiskView.as_view(), name='jituan_fund_freeze_risk'),
|
||||||
|
path('houtai/role-agreement', RoleAgreementAdminView.as_view(), name='jituan_role_agreement'),
|
||||||
path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'),
|
path('houtai/payment-channel-manage', ClubPaymentChannelManageView.as_view(), name='jituan_payment_channel_manage'),
|
||||||
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
path('houtai/club-cert-upload', ClubCertUploadView.as_view(), name='jituan_club_cert_upload'),
|
||||||
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'),
|
||||||
|
|||||||
70
jituan/views_role_agreement.py
Normal file
70
jituan/views_role_agreement.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""客服后台:角色协议配置。"""
|
||||||
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
from jituan.services import role_agreement as svc
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
|
||||||
|
PERM_CODE = 'xycfg001'
|
||||||
|
|
||||||
|
|
||||||
|
def _auth(request, perm_code: str = PERM_CODE):
|
||||||
|
username = (request.data.get('username') or request.data.get('phone') or '').strip()
|
||||||
|
if not username:
|
||||||
|
return None, None, Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return None, None, permissions
|
||||||
|
codes = set(permissions or [])
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
if getattr(user, 'IsSuperuser', False) or '000001' in codes:
|
||||||
|
return kefu, permissions, None
|
||||||
|
if perm_code not in codes:
|
||||||
|
return None, None, Response({'code': 403, 'msg': f'无权限(需要 {perm_code})'})
|
||||||
|
return kefu, permissions, None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAgreementAdminView(APIView):
|
||||||
|
"""
|
||||||
|
POST /jituan/houtai/role-agreement
|
||||||
|
action: get | save | publish | upload
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser, MultiPartParser, FormParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
kefu, permissions, err = _auth(request)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
action = (request.data.get('action') or 'get').strip().lower()
|
||||||
|
operator = getattr(getattr(request, 'user', None), 'UserUID', '') or ''
|
||||||
|
|
||||||
|
if action == 'get':
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': svc.build_admin_payload(request)})
|
||||||
|
|
||||||
|
if action == 'save':
|
||||||
|
data, msg = svc.save_agreement(request, request.data, operator=operator)
|
||||||
|
if msg:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
return Response({'code': 0, 'msg': '已保存', 'data': data})
|
||||||
|
|
||||||
|
if action == 'publish':
|
||||||
|
data, msg = svc.publish_agreement(request, request.data, operator=operator)
|
||||||
|
if msg:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
return Response({'code': 0, 'msg': '已发布新版本', 'data': data})
|
||||||
|
|
||||||
|
if action == 'upload':
|
||||||
|
f = request.FILES.get('file')
|
||||||
|
if not f:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 file'})
|
||||||
|
club_id = resolve_club_id_from_request(request) or ''
|
||||||
|
path, msg = svc.upload_agreement_image(f, club_id, kind='content')
|
||||||
|
if msg:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
return Response({'code': 0, 'msg': '上传成功', 'data': {'url': path, 'path': path}})
|
||||||
|
|
||||||
|
return Response({'code': 400, 'msg': '无效 action'})
|
||||||
@@ -18,7 +18,8 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP
|
|||||||
FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView, ShangjiaCufaTongjiView, ShangjiaFakuanLieBiaoView,\
|
FaKuanJiFenTongJiView, DaShouFaKuanLieBiaoView, ShangjiaCufaTongjiView, ShangjiaFakuanLieBiaoView,\
|
||||||
FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View, \
|
FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View, \
|
||||||
GetUserPhoneView, BindUserPhoneView, BossSearchDashouView, \
|
GetUserPhoneView, BindUserPhoneView, BossSearchDashouView, \
|
||||||
FundFreezeMineView, FundFreezeMyLedgerView, FundFreezeMyLedgerDetailView
|
FundFreezeMineView, FundFreezeMyLedgerView, FundFreezeMyLedgerDetailView, \
|
||||||
|
RoleAgreementCurrentView, RoleAgreementMineView, RoleAgreementSignView
|
||||||
from .tixian_shenhe_views import TixianZddkshApplyView
|
from .tixian_shenhe_views import TixianZddkshApplyView
|
||||||
from .paihang_views import PhbHqsjView
|
from .paihang_views import PhbHqsjView
|
||||||
from .xzj_paihang_views import XzjPhbHqsjView
|
from .xzj_paihang_views import XzjPhbHqsjView
|
||||||
@@ -168,6 +169,9 @@ urlpatterns = [
|
|||||||
path('fund-freeze-mine', FundFreezeMineView.as_view(), name='小程序冻结资金汇总'),
|
path('fund-freeze-mine', FundFreezeMineView.as_view(), name='小程序冻结资金汇总'),
|
||||||
path('fund-freeze-ledger', FundFreezeMyLedgerView.as_view(), name='小程序冻结资金明细'),
|
path('fund-freeze-ledger', FundFreezeMyLedgerView.as_view(), name='小程序冻结资金明细'),
|
||||||
path('fund-freeze-ledger-detail', FundFreezeMyLedgerDetailView.as_view(), name='小程序冻结单详情'),
|
path('fund-freeze-ledger-detail', FundFreezeMyLedgerDetailView.as_view(), name='小程序冻结单详情'),
|
||||||
|
path('role-agreement/current', RoleAgreementCurrentView.as_view(), name='小程序角色协议当前'),
|
||||||
|
path('role-agreement/mine', RoleAgreementMineView.as_view(), name='小程序角色协议列表'),
|
||||||
|
path('role-agreement/sign', RoleAgreementSignView.as_view(), name='小程序角色协议签署'),
|
||||||
path('guanshi_contact', GuanshiContactView.as_view(), name='获取组长'),
|
path('guanshi_contact', GuanshiContactView.as_view(), name='获取组长'),
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ from .fund_freeze_mp import (
|
|||||||
FundFreezeMyLedgerView,
|
FundFreezeMyLedgerView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .role_agreement_mp import (
|
||||||
|
RoleAgreementCurrentView,
|
||||||
|
RoleAgreementMineView,
|
||||||
|
RoleAgreementSignView,
|
||||||
|
)
|
||||||
|
|
||||||
from .admin import (
|
from .admin import (
|
||||||
AdGetOrderList,
|
AdGetOrderList,
|
||||||
AdGetOrderTypes,
|
AdGetOrderTypes,
|
||||||
|
|||||||
@@ -66,6 +66,33 @@ def _profile_withdrawable(user, attr: str, field: str = 'yue') -> str:
|
|||||||
return '0.00'
|
return '0.00'
|
||||||
|
|
||||||
|
|
||||||
|
def _default_freeze_reason() -> str:
|
||||||
|
return (
|
||||||
|
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
|
||||||
|
'按俱乐部规则到期或达标后自动转入可提现余额,冻结中金额不可提现。'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _freeze_display_reasons(club_id: str) -> Dict[str, str]:
|
||||||
|
"""按角色返回冻结说明;无配置时用统一默认文案。"""
|
||||||
|
default = _default_freeze_reason()
|
||||||
|
out = {
|
||||||
|
ROLE_DASHOU: default,
|
||||||
|
ROLE_GUANSHI: default,
|
||||||
|
ROLE_ZUZHANG: default,
|
||||||
|
ROLE_SHENHEGUAN: default,
|
||||||
|
}
|
||||||
|
if not club_id:
|
||||||
|
return out
|
||||||
|
try:
|
||||||
|
for row in ClubFundFreezeConfig.query.filter(club_id=club_id):
|
||||||
|
if row.role in out and (row.remark or '').strip():
|
||||||
|
out[row.role] = row.remark.strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class FundFreezeMineView(APIView):
|
class FundFreezeMineView(APIView):
|
||||||
"""
|
"""
|
||||||
POST /yonghu/fund-freeze-mine
|
POST /yonghu/fund-freeze-mine
|
||||||
@@ -110,17 +137,8 @@ class FundFreezeMineView(APIView):
|
|||||||
total_dongjie = sum((Decimal(r['dongjie_yue']) for r in roles), Decimal('0.00'))
|
total_dongjie = sum((Decimal(r['dongjie_yue']) for r in roles), Decimal('0.00'))
|
||||||
total_withdrawable = sum((Decimal(r['withdrawable']) for r in roles), Decimal('0.00'))
|
total_withdrawable = sum((Decimal(r['withdrawable']) for r in roles), Decimal('0.00'))
|
||||||
|
|
||||||
# 用户可见说明:优先打手规则,否则取任意已配置角色
|
reasons = _freeze_display_reasons(club_id)
|
||||||
display_reason = ''
|
display_reason = reasons.get(ROLE_DASHOU) or _default_freeze_reason()
|
||||||
if club_id:
|
|
||||||
cfgs = list(ClubFundFreezeConfig.query.filter(club_id=club_id, enabled=True))
|
|
||||||
prefer = next((c for c in cfgs if c.role == ROLE_DASHOU), None) or (cfgs[0] if cfgs else None)
|
|
||||||
if prefer and prefer.remark:
|
|
||||||
display_reason = prefer.remark
|
|
||||||
if not display_reason:
|
|
||||||
any_cfg = ClubFundFreezeConfig.query.filter(club_id=club_id).exclude(remark='').first()
|
|
||||||
if any_cfg:
|
|
||||||
display_reason = any_cfg.remark or ''
|
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
@@ -132,10 +150,8 @@ class FundFreezeMineView(APIView):
|
|||||||
'total_dongjie': _money(total_dongjie),
|
'total_dongjie': _money(total_dongjie),
|
||||||
'total_withdrawable': _money(total_withdrawable),
|
'total_withdrawable': _money(total_withdrawable),
|
||||||
'has_freeze': total_dongjie > 0,
|
'has_freeze': total_dongjie > 0,
|
||||||
'display_reason': display_reason or (
|
'display_reason': display_reason,
|
||||||
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
|
'freeze_display_reasons': reasons,
|
||||||
'按俱乐部规则到期或达标后自动转入可提现余额,冻结中金额不可提现。'
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
85
users/views/role_agreement_mp.py
Normal file
85
users/views/role_agreement_mp.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""小程序角色协议只读/签署(勾选确认,无手写)。"""
|
||||||
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
from jituan.services import role_agreement as svc
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAgreementCurrentView(APIView):
|
||||||
|
"""
|
||||||
|
POST /yonghu/role-agreement/current
|
||||||
|
body: { role, gate?, view? }
|
||||||
|
gate: qiangdan|register|tixian|invite — 按卡点判断是否需签
|
||||||
|
view: 1/true — 只读查看,不强制签署
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
role = (request.data.get('role') or '').strip().lower()
|
||||||
|
if not role:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 role', 'data': None})
|
||||||
|
gate = (request.data.get('gate') or '').strip().lower()
|
||||||
|
view_raw = request.data.get('view') or request.data.get('view_only')
|
||||||
|
view_only = view_raw in (True, 'true', '1', 1, 'True')
|
||||||
|
club_id = get_user_club_id(request.user) or ''
|
||||||
|
data = svc.get_current_for_user(
|
||||||
|
club_id, request.user.UserUID, role, gate=gate, view_only=view_only,
|
||||||
|
)
|
||||||
|
return Response({'code': 200, 'msg': 'ok', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAgreementMineView(APIView):
|
||||||
|
"""
|
||||||
|
POST /yonghu/role-agreement/mine
|
||||||
|
body: { roles?: ['dashou','guanshi'] } — 我的协议列表(按俱乐部)
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
roles = request.data.get('roles')
|
||||||
|
if isinstance(roles, str):
|
||||||
|
roles = [x.strip() for x in roles.split(',') if x.strip()]
|
||||||
|
if not isinstance(roles, list):
|
||||||
|
roles = None
|
||||||
|
club_id = get_user_club_id(request.user) or ''
|
||||||
|
data = svc.list_for_user(club_id, request.user.UserUID, roles=roles)
|
||||||
|
return Response({'code': 200, 'msg': 'ok', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class RoleAgreementSignView(APIView):
|
||||||
|
"""
|
||||||
|
POST /yonghu/role-agreement/sign
|
||||||
|
JSON: { role, agreed, client_meta? }
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser, FormParser, MultiPartParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
role = (request.data.get('role') or '').strip().lower()
|
||||||
|
agreed_raw = request.data.get('agreed')
|
||||||
|
agreed = agreed_raw in (True, 'true', '1', 1, 'True')
|
||||||
|
club_id = get_user_club_id(request.user) or ''
|
||||||
|
|
||||||
|
meta = request.data.get('client_meta')
|
||||||
|
if isinstance(meta, str):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
meta = json.loads(meta)
|
||||||
|
except Exception:
|
||||||
|
meta = {'raw': meta}
|
||||||
|
if not isinstance(meta, dict):
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
data, msg = svc.sign_agreement(
|
||||||
|
club_id,
|
||||||
|
request.user.UserUID,
|
||||||
|
role,
|
||||||
|
client_meta=meta,
|
||||||
|
agreed=agreed,
|
||||||
|
)
|
||||||
|
if msg:
|
||||||
|
return Response({'code': 400, 'msg': msg, 'data': None})
|
||||||
|
return Response({'code': 200, 'msg': '签署成功', 'data': data})
|
||||||
@@ -1112,29 +1112,14 @@ class TixianAssetView(APIView):
|
|||||||
|
|
||||||
from jituan.services.club_user import get_user_club_id
|
from jituan.services.club_user import get_user_club_id
|
||||||
from jituan.services.withdraw_config import build_tixian_asset_rates
|
from jituan.services.withdraw_config import build_tixian_asset_rates
|
||||||
from jituan.models import ClubFundFreezeConfig
|
from users.views.fund_freeze_mp import _freeze_display_reasons, _default_freeze_reason
|
||||||
|
|
||||||
club_id = get_user_club_id(user)
|
club_id = get_user_club_id(user)
|
||||||
data.update(build_tixian_asset_rates(club_id))
|
data.update(build_tixian_asset_rates(club_id))
|
||||||
|
|
||||||
display_reason = ''
|
reasons = _freeze_display_reasons(club_id or '')
|
||||||
try:
|
data['freeze_display_reasons'] = reasons
|
||||||
if club_id:
|
data['freeze_display_reason'] = reasons.get('dashou') or _default_freeze_reason()
|
||||||
prefer = ClubFundFreezeConfig.query.filter(
|
|
||||||
club_id=club_id, role='dashou', enabled=True,
|
|
||||||
).first()
|
|
||||||
if not prefer:
|
|
||||||
prefer = ClubFundFreezeConfig.query.filter(
|
|
||||||
club_id=club_id, enabled=True,
|
|
||||||
).exclude(remark='').first()
|
|
||||||
if prefer:
|
|
||||||
display_reason = prefer.remark or ''
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
data['freeze_display_reason'] = display_reason or (
|
|
||||||
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
|
|
||||||
'按规则解冻后自动转入可提现,冻结中不可提现。'
|
|
||||||
)
|
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
|
|||||||
Reference in New Issue
Block a user