"""组队招募核心服务(新域)。无有效组队时所有挂钩函数立即返回,不影响旧逻辑。""" from __future__ import annotations import logging from decimal import Decimal, ROUND_DOWN from typing import Optional from django.db import transaction from django.db.models import F from django.utils import timezone logger = logging.getLogger(__name__) MONEY = Decimal('0.01') def _d(v) -> Decimal: try: return Decimal(str(v or 0)).quantize(MONEY) except Exception: return Decimal('0.00') def _log(club_id, order_id, recruit_id, actor, action, detail=''): try: from orders.models import TeamActionLog TeamActionLog( club_id=(club_id or '')[:16], order_id=(order_id or '')[:32], recruit_id=recruit_id, actor_uid=(actor or '')[:32], action=(action or '')[:32], detail=(detail or '')[:2000], ).save() except Exception as e: logger.warning('TeamActionLog 失败: %s', e) def calc_display_share(commission: Decimal, mode: str, need_count: int, per_person: Decimal) -> Decimal: c = _d(commission) need = max(1, min(3, int(need_count or 1))) if mode == 'fixed': return _d(per_person) # average: C / (1+need) n = 1 + need if n <= 0 or c <= 0: return Decimal('0.00') return (c / Decimal(n)).quantize(MONEY, rounding=ROUND_DOWN) def calc_lock_shares(commission: Decimal, mode: str, per_person: Decimal, leader_uid: str, member_uids: list) -> dict: """返回 {uid: amount},合计等于 C(尾差给队长)。member_uids 不含队长。""" c = _d(commission) members = [u for u in (member_uids or []) if u and u != leader_uid] n = 1 + len(members) out = {} if n <= 0 or c <= 0: return {leader_uid: c} if leader_uid else {} if mode == 'fixed': p = _d(per_person) for uid in members: out[uid] = p out[leader_uid] = _d(c - p * len(members)) if out[leader_uid] < 0: out[leader_uid] = Decimal('0.00') return out base = (c / Decimal(n)).quantize(MONEY, rounding=ROUND_DOWN) for uid in members: out[uid] = base out[leader_uid] = _d(c - base * len(members)) return out def get_active_recruit(order_id: str): from orders.models import TeamRecruit return TeamRecruit.query.filter( order_id=order_id, status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED], ).first() def team_should_split(order) -> bool: """结单是否走组队分账:已锁定(或招募中但已有人——进8会强制锁)且有在队成员。""" from orders.models import TeamRecruit, TeamMember oid = getattr(order, 'OrderID', None) or '' if not oid: return False rec = TeamRecruit.query.filter( order_id=oid, status=TeamRecruit.STATUS_LOCKED, ).first() if not rec: return False return TeamMember.query.filter( recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE, ).exists() def _set_dashou_busy(uid: str, busy: bool): from users.models import UserDashou from users.business_models import User u = User.query.filter(UserUID=uid).first() if not u: return UserDashou.objects.filter(user=u).update(zhuangtai=0 if busy else 1) def _subscribe_uids_to_order_group(order_id: str, uids: list): """订阅 Ds{uid} 到 group_{order_id}。失败只打日志。""" try: from utils.chat_utils import _subscribe_users_to_group, _get_self_goeasy_appkey, _get_self_goeasy_secret appkey = _get_self_goeasy_appkey() secret = _get_self_goeasy_secret() if not appkey: return False user_ids = [f'Ds{u}' for u in uids if u] if not user_ids: return False return _subscribe_users_to_group(user_ids, [f'group_{order_id}'], appkey, secret) except Exception as e: logger.warning('组队拉群失败 order=%s: %s', order_id, e) return False @transaction.atomic def open_recruit( *, order, leader_uid: str, need_count: int, share_mode: str, per_person_amount=None, remark: str = '', leader_game_id: str = '', min_deal_rate=None, max_fine_rate=None, ): from orders.models import TeamRecruit, TeamMember oid = order.OrderID club_id = (getattr(order, 'ClubID', None) or '').strip() or 'xq' if int(order.Status or 0) != 2: raise ValueError('仅进行中订单可开启组队') if (order.PlayerID or '') != leader_uid: raise ValueError('仅接单打手可开启组队') if TeamRecruit.query.filter(order_id=oid).exclude( status__in=[TeamRecruit.STATUS_CANCELLED, TeamRecruit.STATUS_VOID, TeamRecruit.STATUS_FINISHED] ).exists(): raise ValueError('本单已有进行中的组队') need = int(need_count or 0) if need < 1 or need > 3: raise ValueError('招募人数须为1~3') mode = (share_mode or '').strip().lower() if mode not in (TeamRecruit.MODE_FIXED, TeamRecruit.MODE_AVERAGE): raise ValueError('请选择固定值或平均值') c = _d(order.PlayerCommission) if c <= 0: raise ValueError('打手分成无效') p = _d(per_person_amount) if mode == TeamRecruit.MODE_FIXED: if p <= 0: raise ValueError('请设置固定人均金额') if p * (1 + need) > c: raise ValueError('人均×(队长+目标人数)不能超过打手分成') game_id = (leader_game_id or '').strip() if not game_id: raise ValueError('请填写队长游戏ID') display = calc_display_share(c, mode, need, p) now = timezone.now() rec = TeamRecruit( club_id=club_id, order_id=oid, leader_uid=leader_uid, status=TeamRecruit.STATUS_RECRUITING, need_count=need, joined_count=0, share_mode=mode, per_person_amount=p if mode == TeamRecruit.MODE_FIXED else Decimal('0.00'), display_share_amount=display, commission_snap=c, order_amount_snap=_d(order.Amount), remark=(remark or '')[:200], leader_game_id=game_id[:64], min_deal_rate=_d(min_deal_rate) if min_deal_rate not in (None, '') else None, max_fine_rate=_d(max_fine_rate) if max_fine_rate not in (None, '') else None, opened_at=now, ) # Decimal null fields if min_deal_rate in (None, ''): rec.min_deal_rate = None if max_fine_rate in (None, ''): rec.max_fine_rate = None rec.save() TeamMember( club_id=club_id, recruit_id=rec.id, order_id=oid, user_uid=leader_uid, role=TeamMember.ROLE_LEADER, game_id=game_id[:64], share_amount=Decimal('0.00'), join_status=TeamMember.JOIN_ACTIVE, joined_at=now, ).save() _subscribe_uids_to_order_group(oid, [leader_uid]) _log(club_id, oid, rec.id, leader_uid, 'open', f'mode={mode} need={need} display={display}') return rec @transaction.atomic def update_recruit(*, recruit_id: int, leader_uid: str, need_count=None, remark=None, min_deal_rate='__omit__', max_fine_rate='__omit__'): from orders.models import TeamRecruit, Order rec = TeamRecruit.objects.select_for_update().filter(id=recruit_id).first() if not rec: raise ValueError('组队不存在') if rec.leader_uid != leader_uid: raise ValueError('仅队长可修改') if rec.status != TeamRecruit.STATUS_RECRUITING: raise ValueError('仅招募中可修改') order = Order.query.filter(OrderID=rec.order_id).first() if not order or int(order.Status or 0) != 2: raise ValueError('订单非进行中,无法修改招募') if need_count is not None: need = int(need_count) if need < 1 or need > 3: raise ValueError('招募人数须为1~3') if need < int(rec.joined_count or 0): raise ValueError('目标人数不能小于已入队人数') if rec.share_mode == TeamRecruit.MODE_FIXED: p = _d(rec.per_person_amount) c = _d(order.PlayerCommission) or _d(rec.commission_snap) if p * (1 + need) > c: raise ValueError('人均×(队长+目标人数)不能超过打手分成') rec.need_count = need if remark is not None: rec.remark = (remark or '')[:200] if min_deal_rate != '__omit__': rec.min_deal_rate = _d(min_deal_rate) if min_deal_rate not in (None, '') else None if max_fine_rate != '__omit__': rec.max_fine_rate = _d(max_fine_rate) if max_fine_rate not in (None, '') else None c = _d(order.PlayerCommission) or _d(rec.commission_snap) rec.display_share_amount = calc_display_share( c, rec.share_mode, rec.need_count, rec.per_person_amount, ) rec.save() _log(rec.club_id, rec.order_id, rec.id, leader_uid, 'update', f'need={rec.need_count}') return rec @transaction.atomic def lock_recruit(rec_or_id, *, actor_uid='', reason='manual'): from orders.models import TeamRecruit, TeamMember, Order rid = rec_or_id if isinstance(rec_or_id, int) else rec_or_id.id rec = TeamRecruit.objects.select_for_update().filter(id=rid).first() if not rec: return None if rec.status == TeamRecruit.STATUS_LOCKED: return rec if rec.status != TeamRecruit.STATUS_RECRUITING: raise ValueError('当前状态不可锁定') order = Order.query.filter(OrderID=rec.order_id).first() c = _d(getattr(order, 'PlayerCommission', None) if order else None) or _d(rec.commission_snap) members = list( TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE) .exclude(role=TeamMember.ROLE_LEADER) .values_list('user_uid', flat=True) ) shares = calc_lock_shares(c, rec.share_mode, rec.per_person_amount, rec.leader_uid, list(members)) now = timezone.now() for uid, amt in shares.items(): TeamMember.objects.filter( recruit_id=rec.id, user_uid=uid, join_status=TeamMember.JOIN_ACTIVE, ).update(share_amount=amt, UpdateTime=now) rec.status = TeamRecruit.STATUS_LOCKED rec.locked_at = now rec.display_share_amount = shares.get( next(iter(members), rec.leader_uid), calc_display_share(c, rec.share_mode, rec.need_count, rec.per_person_amount), ) # 队友展示用:取任一队友份额,无队友则用队长外均分预览 if members: rec.display_share_amount = shares.get(members[0], Decimal('0.00')) rec.save() _log(rec.club_id, rec.order_id, rec.id, actor_uid, 'lock', reason) return rec def on_order_enter_status_8(order): """进结算中:招募中强制锁定。不改 eligible。""" try: from orders.models import TeamRecruit oid = getattr(order, 'OrderID', '') or '' rec = TeamRecruit.query.filter(order_id=oid, status=TeamRecruit.STATUS_RECRUITING).first() if rec: lock_recruit(rec.id, actor_uid='system', reason='enter_status_8') except Exception as e: logger.error('组队进8锁定失败: %s', e) @transaction.atomic def join_recruit(*, recruit_id: int, user_uid: str, game_id: str, request=None, require_club_id=None): from orders.models import TeamRecruit, TeamMember, Order from users.models import UserDashou from utils.fadan_utils import check_fadan_qiangdan_eligible game_id = (game_id or '').strip() if not game_id: raise ValueError('请填写游戏ID') rec = TeamRecruit.objects.select_for_update().filter(id=recruit_id).first() if not rec: raise ValueError('组队不存在') # 组队强制本俱乐部:不认跨店互通抢单 if require_club_id and (rec.club_id or '') != require_club_id: raise ValueError('仅本俱乐部可加入组队') if rec.status != TeamRecruit.STATUS_RECRUITING: raise ValueError('招募已结束') if int(rec.joined_count or 0) >= int(rec.need_count or 0): raise ValueError('人数已满') if rec.leader_uid == user_uid: raise ValueError('队长无需加入') if TeamMember.query.filter(recruit_id=rec.id, user_uid=user_uid, join_status=TeamMember.JOIN_ACTIVE).exists(): raise ValueError('您已在队中') order = Order.query.filter(OrderID=rec.order_id).first() if not order or int(order.Status or 0) != 2: raise ValueError('订单状态不可加入') if (getattr(order, 'ClubID', '') or '') != rec.club_id: raise ValueError('俱乐部不匹配') if require_club_id and (order.ClubID or '') != require_club_id: raise ValueError('仅本俱乐部订单可加入组队') # 忙碌 / 罚单 try: from users.business_models import User user_obj = User.query.filter(UserUID=user_uid).first() if not user_obj: raise ValueError('用户不存在') profile = UserDashou.query.filter(user=user_obj).first() if not profile: raise ValueError('非打手身份') if int(getattr(profile, 'zhuangtai', 0) or 0) != 1: raise ValueError('您有订单正在接待中') if int(getattr(profile, 'zhanghaozhuangtai', 1) or 1) != 1: raise ValueError('账号状态异常') except ValueError: raise except Exception: raise ValueError('打手信息异常') ok, msg = check_fadan_qiangdan_eligible(user_uid, 1) if not ok: raise ValueError(msg or '存在未处理罚单') # 不能同时在其它进行中组队 busy_other = TeamMember.query.filter( user_uid=user_uid, join_status=TeamMember.JOIN_ACTIVE, role=TeamMember.ROLE_MEMBER, ).exclude(order_id=rec.order_id) if busy_other.exists(): # 仅当对应帖仍 recruiting/locked from orders.models import TeamRecruit as TR for m in busy_other[:5]: r = TR.query.filter(id=m.recruit_id, status__in=[TR.STATUS_RECRUITING, TR.STATUS_LOCKED]).first() if r: raise ValueError('您已在其他组队中') # 可选指标门槛 if rec.min_deal_rate is not None or rec.max_fine_rate is not None: from jituan.models import DashouDealStat from jituan.services.deal_stats import serialize_dashou_stat row = DashouDealStat.query.filter(club_id=rec.club_id, dashou_uid=user_uid).first() deal_rate = 0.0 fine_rate = 0.0 if row and int(row.order_count or 0) > 0: s = serialize_dashou_stat(row) deal_rate = float(s.get('deal_rate') or 0) fine_rate = float(s.get('fined_rate') or 0) if rec.min_deal_rate is not None and deal_rate < float(rec.min_deal_rate): raise ValueError('成交率未达到队长要求') if rec.max_fine_rate is not None and fine_rate > float(rec.max_fine_rate): raise ValueError('罚款率超过队长要求') # 原单会员/押金 if order.GrabRequirement == 1: from jituan.services.huiyuan_bundle import user_has_huiyuan_access_any, resolve_order_membership_ids required_ids = resolve_order_membership_ids({ 'MembershipID': order.MembershipID, 'GrabRequirement': order.GrabRequirement, 'ProductTypeID': order.ProductTypeID, }) if not user_has_huiyuan_access_any(user_uid, required_ids, club_id=rec.club_id): raise ValueError('未开通对应会员或会员已过期') elif order.GrabRequirement == 2: yajin_required = order.CommissionReq or 0 if (profile.yajin or 0) < yajin_required: raise ValueError(f'押金不足,需要{yajin_required}元') now = timezone.now() TeamMember( club_id=rec.club_id, recruit_id=rec.id, order_id=rec.order_id, user_uid=user_uid, role=TeamMember.ROLE_MEMBER, game_id=game_id[:64], share_amount=_d(rec.display_share_amount) if rec.share_mode == TeamRecruit.MODE_FIXED else Decimal('0.00'), join_status=TeamMember.JOIN_ACTIVE, joined_at=now, ).save() TeamRecruit.objects.filter(id=rec.id).update(joined_count=F('joined_count') + 1, UpdateTime=now) rec.refresh_from_db() _set_dashou_busy(user_uid, True) _subscribe_uids_to_order_group(rec.order_id, [user_uid, rec.leader_uid]) _log(rec.club_id, rec.order_id, rec.id, user_uid, 'join', game_id) if int(rec.joined_count or 0) >= int(rec.need_count or 0): lock_recruit(rec.id, actor_uid=user_uid, reason='full') rec.refresh_from_db() return rec @transaction.atomic def clear_recruit(*, order_id: str, status: int, actor_uid='system', reason=''): """取消/作废:清退成员、置空闲、零分账。status=4取消或5作废。""" from orders.models import TeamRecruit, TeamMember rec = TeamRecruit.objects.select_for_update().filter(order_id=order_id).exclude( status__in=[TeamRecruit.STATUS_FINISHED, TeamRecruit.STATUS_CANCELLED, TeamRecruit.STATUS_VOID], ).first() if not rec: return None members = list( TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE).values_list('user_uid', 'role') ) now = timezone.now() TeamMember.objects.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE).update( join_status=TeamMember.JOIN_CLEARED, UpdateTime=now, ) for uid, role in members: if role == TeamMember.ROLE_MEMBER: _set_dashou_busy(uid, False) # 队长忙碌由订单主流程管理,此处不强制改队长 rec.status = status rec.closed_at = now rec.save(update_fields=['status', 'closed_at', 'UpdateTime']) _log(rec.club_id, order_id, rec.id, actor_uid, 'clear', f'{reason} ->{status}') return rec def on_order_refunded(order): try: from orders.models import TeamRecruit clear_recruit(order_id=order.OrderID, status=TeamRecruit.STATUS_CANCELLED, reason='refund') except Exception as e: logger.error('组队退款清退失败: %s', e) def on_order_change_player(order_id: str, actor_uid='system'): try: from orders.models import TeamRecruit clear_recruit(order_id=order_id, status=TeamRecruit.STATUS_VOID, actor_uid=actor_uid, reason='change_player') except Exception as e: logger.error('组队换打手作废失败: %s', e) @transaction.atomic def settle_team_or_legacy(order, *, set_idle_leader=False) -> bool: """ 若应组队分账则多人入账并返回 True;否则返回 False 由调用方走原逻辑。 调用方须已在 atomic 且 ideally select_for_update 订单。 """ from orders.models import TeamRecruit, TeamMember, TeamSettleLedger, Order from users.business_models import User from jituan.services.fund_freeze import credit_dashou_order_settle oid = order.OrderID if not team_should_split(order): return False rec = TeamRecruit.objects.select_for_update().filter( order_id=oid, status=TeamRecruit.STATUS_LOCKED, ).first() if not rec: return False members = list( TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE) ) if not members: return False c = _d(order.PlayerCommission) # 若份额未写齐,按锁定规则重算 if any(_d(m.share_amount) <= 0 and m.role == TeamMember.ROLE_MEMBER for m in members) or \ sum((_d(m.share_amount) for m in members), Decimal('0')) <= 0: mate_uids = [m.user_uid for m in members if m.role == TeamMember.ROLE_MEMBER] shares = calc_lock_shares(c, rec.share_mode, rec.per_person_amount, rec.leader_uid, mate_uids) for m in members: m.share_amount = shares.get(m.user_uid, Decimal('0.00')) m.save(update_fields=['share_amount', 'UpdateTime']) total = sum((_d(m.share_amount) for m in members), Decimal('0.00')) if total != c: # 尾差修正到队长 diff = _d(c - total) for m in members: if m.role == TeamMember.ROLE_LEADER: m.share_amount = _d(m.share_amount) + diff m.save(update_fields=['share_amount', 'UpdateTime']) break for m in members: amt = _d(m.share_amount) biz_id = f'{oid}:team:{m.user_uid}' if TeamSettleLedger.query.filter(biz_id=biz_id).exists(): continue try: user = User.query.get(UserUID=m.user_uid) profile = user.DashouProfile except Exception: logger.error('组队分账打手不存在 %s %s', oid, m.user_uid) continue credit_dashou_order_settle( order=order, dashou_profile=profile, user_id=m.user_uid, amount=amt, set_idle_status=True, increment_chengjiao=True, settle_biz_id=biz_id, ) TeamSettleLedger( club_id=rec.club_id, order_id=oid, user_uid=m.user_uid, amount=amt, biz_id=biz_id, ).save() now = timezone.now() rec.status = TeamRecruit.STATUS_FINISHED rec.closed_at = now rec.save(update_fields=['status', 'closed_at', 'UpdateTime']) _log(rec.club_id, oid, rec.id, 'system', 'settle', f'total={c}') return True def maybe_split_penalties(*, order_id: str, total_amount, reason: str, applicant_id, applicant_identity, affects_grabbing=1, club_id='', extra_fields=None) -> Optional[list]: """ 若有有效组队(锁定或招募中已有队友),为每人创建罚单并返回列表;否则返回 None 走原逻辑。 """ from orders.models import TeamRecruit, TeamMember, Penalty from decimal import Decimal as D rec = TeamRecruit.query.filter( order_id=order_id, status__in=[TeamRecruit.STATUS_RECRUITING, TeamRecruit.STATUS_LOCKED], ).first() if not rec: return None members = list( TeamMember.query.filter(recruit_id=rec.id, join_status=TeamMember.JOIN_ACTIVE) ) if len(members) < 2: return None total = _d(total_amount) k = len(members) base = (total / D(k)).quantize(MONEY, rounding=ROUND_DOWN) amounts = [base] * k amounts[0] = _d(total - base * (k - 1)) # 尾差给列表第一人(队长优先排前面) # 队长优先 members.sort(key=lambda m: 0 if m.role == TeamMember.ROLE_LEADER else 1) amounts = [base] * k amounts[0] = _d(total - base * (k - 1)) reason_text = f'【组队均摊】{reason or ""}' created = [] cid = (club_id or rec.club_id or 'xq')[:16] for i, m in enumerate(members): p = Penalty( PenalizedUserID=m.user_uid, ApplicantID=applicant_id, Identity=1, Reason=reason_text, FineAmount=amounts[i], RelatedOrderID=order_id, Status=1, AffectsGrabbing=affects_grabbing, ApplicantIdentity=applicant_identity, ClubID=cid, ) p.save() created.append(p) # 调用方负责 lock_penalty_bonus / 证据图;此处只创建罚单记录 _log(rec.club_id, order_id, rec.id, applicant_id or '', 'penalty_split', f'n={k} total={total}') return created def serialize_recruit_public(rec, order=None, *, include_game_ids=False) -> dict: """大厅公开字段;游戏ID默认不返回。""" from orders.models import Order, MerchantOrderExt from jituan.services.order_deal import order_auto_settle_payload from jituan.models import DashouDealStat from jituan.services.deal_stats import serialize_dashou_stat from users.business_models import User from backend.utils import fmt_datetime order = order or Order.query.filter(OrderID=rec.order_id).first() need = int(rec.need_count or 0) joined = int(rec.joined_count or 0) data = { 'recruit_id': rec.id, 'order_id': rec.order_id, 'club_id': rec.club_id, 'status': rec.status, 'need_count': need, 'joined_count': joined, 'remain_count': max(0, need - joined), 'share_mode': rec.share_mode, 'display_share_amount': float(_d(rec.display_share_amount)), 'per_person_amount': float(_d(rec.per_person_amount)), 'remark': rec.remark or '', 'leader_uid': rec.leader_uid, 'leader_nicheng': '', 'min_deal_rate': float(rec.min_deal_rate) if rec.min_deal_rate is not None else None, 'max_fine_rate': float(rec.max_fine_rate) if rec.max_fine_rate is not None else None, 'opened_at': fmt_datetime(rec.opened_at) if rec.opened_at else '', 'locked_at': fmt_datetime(rec.locked_at) if rec.locked_at else '', 'fadanpingtai': getattr(order, 'Platform', None) if order else None, 'fadanpingtai_text': '', 'leixing_id': getattr(order, 'ProductTypeID', None) if order else None, 'jieshao': (order.Description or '') if order else '', 'tupian': (order.ImageURL or '') if order else '', 'order_zhuangtai': getattr(order, 'Status', None) if order else None, 'shangjia_nicheng': '', 'leader_deal_stat': None, } if order: data['fadanpingtai_text'] = '平台单' if order.Platform == 1 else ('商家单' if order.Platform == 2 else '') if order.Platform == 2: try: ext = order.shangjia_kuozhan data['shangjia_nicheng'] = ext.MerchantNickname or '' except Exception: pass # 自动结算字段:仅锁定且订单结算中 if rec.status == 2 and int(order.Status or 0) == 8: data.update(order_auto_settle_payload(order)) try: u = User.query.filter(UserUID=rec.leader_uid).select_related('DashouProfile').first() if u and getattr(u, 'DashouProfile', None): data['leader_nicheng'] = u.DashouProfile.nicheng or '' except Exception: pass row = DashouDealStat.query.filter(club_id=rec.club_id, dashou_uid=rec.leader_uid).first() if row and int(row.order_count or 0) > 0: s = serialize_dashou_stat(row) data['leader_deal_stat'] = { 'deal_rate': s['deal_rate'], 'fined_rate': s['fined_rate'], 'deal_rate_text': f"{round(s['deal_rate'] * 100, 1)}%", 'fined_rate_text': f"{round(s['fined_rate'] * 100, 1)}%", } if include_game_ids: from orders.models import TeamMember mems = TeamMember.query.filter(recruit_id=rec.id, join_status=1) data['members'] = [ { 'user_uid': m.user_uid, 'role': m.role, 'game_id': m.game_id, 'share_amount': float(_d(m.share_amount)), } for m in mems ] return data