from rest_framework.response import Response from .models import Dianpu, YonghuPingzheng from products.models import Shangpin from .models import Dianpu, DianpuMorenPeizhi # shangdian/utils.py 或 tongji/utils.py import logging from datetime import date from decimal import Decimal from django.db import transaction from django.db.models import F from .models import Dianpu, DianpuShouzhiMeiriTongji logger = logging.getLogger(__name__) def verify_shop_permission(request, frontend_user_id=None): """ 验证商家后台用户身份及店铺状态 参数: request: DRF request 对象 frontend_user_id: 前端传递的用户ID(用于防越权校验,可选) 返回: (dianpu_obj, None) # 验证成功,返回店铺对象 (None, Response) # 验证失败,返回错误响应 """ current_user = request.user # 1. 从 request.user 获取当前认证用户的 yonghuid # 注意:您的用户模型可能通过 JWT 的 sub 或其他字段标识,此处假设 request.user.yonghuid 存在 if not hasattr(current_user, 'yonghuid'): logger.error(f"用户对象缺少 yonghuid 属性: {current_user}") return None, Response( {'code': 500, 'msg': '用户身份信息异常'} ) auth_user_id = current_user.yonghuid # 2. 越权检测:如果前端传了用户ID,必须与当前登录用户一致 if frontend_user_id is not None and str(frontend_user_id) != str(auth_user_id): logger.warning( f"越权尝试: 登录用户 {auth_user_id} 尝试操作 {frontend_user_id} 的数据" ) return None, Response( {'code': 403, 'msg': '无权操作其他用户数据'} ) # 3. 查询用户凭证表,验证账号是否存在且状态正常 try: pingzheng = YonghuPingzheng.objects.get(yonghu=auth_user_id) if not pingzheng.is_active: return None, Response( {'code': 403, 'msg': '用户账号已被禁用'} ) except YonghuPingzheng.DoesNotExist: return None, Response( {'code': 404, 'msg': '用户凭证不存在'} ) # 4. 查询关联的店铺信息,验证店铺状态 try: dianpu = Dianpu.objects.get(pingzheng=pingzheng) if dianpu.zhuangtai != 1: # 1 表示正常 return None, Response( {'code': 403, 'msg': '店铺已被封禁'} ) except Dianpu.DoesNotExist: return None, Response( {'code': 404, 'msg': '店铺信息不存在'} ) # 验证通过,返回店铺对象 return dianpu, None '''def update_dianpu_daily_stat( dianpu_id, caozuo_leixing, dingdan_jiage=Decimal('0.00'), dianpu_shouyi=Decimal('0.00') ): """ 原子更新店铺每日统计数据,并根据结算操作更新店铺余额与总额(高并发安全) 参数: dianpu_id: 店铺ID (int) caozuo_leixing: 操作类型 1 = 下单 2 = 结算(成交) 3 = 退款 dingdan_jiage: 订单金额(商品价格),默认0.00 dianpu_shouyi: 店铺本次收益金额,默认0.00 注意: - 金额若小于0则自动设为0 - 日期自动取当天 - 使用 select_for_update 配合事务,保证并发时数据绝对准确 - 若店铺不存在,仅记录日志不中断主流程 - 结算操作会同步更新 Dianpu 表的 ketixian_yue 和 zhengqu_zonge """ # 参数归一化与合法性处理 dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') dianpu_shouyi = Decimal(str(dianpu_shouyi)) if dianpu_shouyi else Decimal('0.00') if dingdan_jiage < 0: dingdan_jiage = Decimal('0.00') if dianpu_shouyi < 0: dianpu_shouyi = Decimal('0.00') today = date.today() # 校验店铺是否存在(不加锁,仅校验) if not Dianpu.objects.filter(id=dianpu_id).exists(): logger.error(f"店铺ID {dianpu_id} 不存在,统计更新失败") return # 开启事务,使用行锁确保并发安全 with transaction.atomic(): # 1. 获取或创建统计记录,并锁定该行 tongji, created = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( dianpu_id=dianpu_id, riqi=today, defaults={ 'nian': today.year, 'yue': today.month, 'ri': today.day, 'xiadan_zongliang': 0, 'xiadan_zonge': Decimal('0.00'), 'chengjiao_zongliang': 0, 'chengjiao_zonge': Decimal('0.00'), 'tuikuan_zongliang': 0, 'tuikuan_zonge': Decimal('0.00'), 'shouyi_zonge': Decimal('0.00'), } ) # 根据操作类型构建统计更新字段 update_fields = {} if caozuo_leixing == 1: # 下单 update_fields['xiadan_zongliang'] = F('xiadan_zongliang') + 1 update_fields['xiadan_zonge'] = F('xiadan_zonge') + dingdan_jiage elif caozuo_leixing == 2: # 结算(成交) update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + dingdan_jiage update_fields['shouyi_zonge'] = F('shouyi_zonge') + dianpu_shouyi # 结算时同步更新店铺表的可提现余额和赚取总额(需加锁店铺行) try: dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) dianpu.ketixian_yue = F('ketixian_yue') + dianpu_shouyi dianpu.zhengqu_zonge = F('zhengqu_zonge') + dianpu_shouyi dianpu.save(update_fields=['ketixian_yue', 'zhengqu_zonge', 'update_time']) except Dianpu.DoesNotExist: logger.error(f"结算时店铺ID {dianpu_id} 不存在,但已校验过,此处不应发生") raise elif caozuo_leixing == 3: # 退款 update_fields['tuikuan_zongliang'] = F('tuikuan_zongliang') + 1 update_fields['tuikuan_zonge'] = F('tuikuan_zonge') + dingdan_jiage # 退款是否扣减收益需根据业务决定,此处只累加退款统计 else: logger.warning(f"未知的操作类型: {caozuo_leixing}") return # 原子更新统计字段 if update_fields: for field, value in update_fields.items(): setattr(tongji, field, value) tongji.save(update_fields=list(update_fields.keys())) logger.info( f"店铺统计更新: 店铺ID={dianpu_id}, 操作类型={caozuo_leixing}, " f"订单金额={dingdan_jiage}, 店铺收益={dianpu_shouyi}, 日期={today}" ) def calculate_pingtai_and_dianpu_shouyi(shangpin_id, dingdan_jiage, dashou_fencheng): """ 根据商品价格和打手分成计算平台收益和店铺收益 逻辑: 总收益 = 订单金额 - 打手分成 1. 从商品表获取所属店铺ID 2. 若商品无店铺ID:平台收益 = 总收益,店铺收益 = 0 3. 若有店铺ID: - 查询店铺配置: * 若店铺开启抽成,则费率 = 店铺表.shouyi_choucheng_feilv * 否则从店铺默认配置表(ID=1)获取默认费率,若记录不存在则自动创建 - 平台收益 = 总收益 * 费率 - 店铺收益 = 总收益 - 平台收益 参数: shangpin_id: 商品ID (int) dingdan_jiage: 订单金额(商品价格)(Decimal 或 数字) dashou_fencheng: 打手分成金额 (Decimal 或 数字) 返回: (dianpu_id, pingtai_shouyi, dianpu_shouyi) 三元组 dianpu_id 可能为 None pingtai_shouyi 必定有值 dianpu_shouyi 必定有值 """ # 类型转换与安全处理 dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') dashou_fencheng = Decimal(str(dashou_fencheng)) if dashou_fencheng else Decimal('0.00') if dingdan_jiage < 0: dingdan_jiage = Decimal('0.00') if dashou_fencheng < 0: dashou_fencheng = Decimal('0.00') # 计算总收益(可用于抽成的基础) zong_shouyi = dingdan_jiage - dashou_fencheng if zong_shouyi < 0: zong_shouyi = Decimal('0.00') # 获取商品信息及所属店铺 try: shangpin = Shangpin.objects.select_related('dianpu').only('id', 'dianpu').get(id=shangpin_id) except Shangpin.DoesNotExist: logger.error(f"计算收益失败:商品ID {shangpin_id} 不存在") return None, zong_shouyi, Decimal('0.00') # 无商品时平台收益为总收益,店铺0 dianpu_id = shangpin.dianpu_id # 无店铺ID:平台拿走全部总收益,店铺收益为0 if dianpu_id is None: logger.info(f"商品 {shangpin_id} 未关联店铺,平台收益 = 总收益 {zong_shouyi}") return None, zong_shouyi, Decimal('0.00') # 有店铺ID:查询店铺抽成配置 try: dianpu = Dianpu.objects.only( 'id', 'kaiqi_shouyi_choucheng', 'shouyi_choucheng_feilv' ).get(id=dianpu_id) except Dianpu.DoesNotExist: logger.error(f"计算收益失败:店铺ID {dianpu_id} 不存在") return dianpu_id, zong_shouyi, Decimal('0.00') # 店铺不存在时平台收益也为总收益 if dianpu.kaiqi_shouyi_choucheng and dianpu.shouyi_choucheng_feilv is not None: feilv = dianpu.shouyi_choucheng_feilv else: # 从默认配置表获取或创建 moren_peizhi, created = DianpuMorenPeizhi.objects.get_or_create( id=1, defaults={ 'moren_choucheng_feilv': Decimal('0.1000'), # 默认10% 'moren_tixian_xiane': Decimal('1000.00') } ) feilv = moren_peizhi.moren_choucheng_feilv # 确保费率在合法范围内 if feilv is None or feilv < 0: feilv = Decimal('0.0000') if feilv > 1: feilv = Decimal('1.0000') # 计算平台收益(费率 * 总收益) pingtai_shouyi = zong_shouyi * feilv # 店铺收益 = 总收益 - 平台收益 dianpu_shouyi = zong_shouyi - pingtai_shouyi logger.info( f"收益计算: 商品ID={shangpin_id}, 订单金额={dingdan_jiage}, " f"打手分成={dashou_fencheng}, 总收益={zong_shouyi}, 费率={feilv}, " f"平台收益={pingtai_shouyi}, 店铺收益={dianpu_shouyi}" ) return dianpu_id, pingtai_shouyi, dianpu_shouyi''' def update_dianpu_daily_stat( dianpu_id, caozuo_leixing, dingdan_jiage=Decimal('0.00'), dianpu_shouyi=Decimal('0.00'), shangji_dianpu_id=None, # 新增:上级店铺ID shangji_fenhong=Decimal('0.00') # 新增:该笔订单给上级的分红 ): """ 原子更新店铺每日统计数据,结算时同步更新店铺余额,并安全处理上级分红统计。 参数: dianpu_id: 本店铺ID caozuo_leixing: 1=下单, 2=结算, 3=退款 dingdan_jiage: 订单金额 dianpu_shouyi: 店铺自身收益 shangji_dianpu_id: 上级店铺ID(可选) shangji_fenhong: 给上级的分红金额(可选) 注意: - 金额自动归一化,负数取0 - 使用 select_for_update 行锁保证并发安全 - 结算时,本店余额和统计先落定,上级分红统计放在 try 块内,失败不影响本店 - 新增字段均包含在统计表中:给上级次数/总额/上级ID,以及作为上级获得次数/总额 """ # 金额安全处理 dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') dianpu_shouyi = Decimal(str(dianpu_shouyi)) if dianpu_shouyi else Decimal('0.00') shangji_fenhong = Decimal(str(shangji_fenhong)) if shangji_fenhong else Decimal('0.00') if dingdan_jiage < 0: dingdan_jiage = Decimal('0.00') if dianpu_shouyi < 0: dianpu_shouyi = Decimal('0.00') if shangji_fenhong < 0: shangji_fenhong = Decimal('0.00') today = date.today() # 轻量校验店铺存在性 if not Dianpu.objects.filter(id=dianpu_id).exists(): logger.error(f"店铺ID {dianpu_id} 不存在,统计更新失败") return # 开启事务,全部使用行锁 with transaction.atomic(): # 1. 获取或创建本店今日统计记录并锁定 tongji, created = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( dianpu_id=dianpu_id, riqi=today, defaults={ 'nian': today.year, 'yue': today.month, 'ri': today.day, 'xiadan_zongliang': 0, 'xiadan_zonge': Decimal('0.00'), 'chengjiao_zongliang': 0, 'chengjiao_zonge': Decimal('0.00'), 'tuikuan_zongliang': 0, 'tuikuan_zonge': Decimal('0.00'), 'shouyi_zonge': Decimal('0.00'), # 新增字段默认值 'shangji_fenhong_cishu': 0, 'shangji_fenhong_zonge': Decimal('0.00'), 'gei_shangji_cishu': 0, 'gei_shangji_zonge': Decimal('0.00'), 'shangji_id': None, } ) # 构建更新字段字典 update_fields_dict = {} if caozuo_leixing == 1: # 下单 update_fields_dict['xiadan_zongliang'] = F('xiadan_zongliang') + 1 update_fields_dict['xiadan_zonge'] = F('xiadan_zonge') + dingdan_jiage elif caozuo_leixing == 2: # 结算 # --- 本店成交统计 --- update_fields_dict['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 update_fields_dict['chengjiao_zonge'] = F('chengjiao_zonge') + dingdan_jiage update_fields_dict['shouyi_zonge'] = F('shouyi_zonge') + dianpu_shouyi # --- 更新本店主表余额(加锁) --- try: dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id) dianpu_locked.ketixian_yue = F('ketixian_yue') + dianpu_shouyi dianpu_locked.zhengqu_zonge = F('zhengqu_zonge') + dianpu_shouyi dianpu_locked.save(update_fields=['ketixian_yue', 'zhengqu_zonge', 'update_time']) except Dianpu.DoesNotExist: logger.error(f"结算时店铺 {dianpu_id} 不存在") raise # --- 处理给上级的分红(本店作为下级)--- if shangji_dianpu_id and shangji_fenhong > 0: # 更新本店统计的“给上级”字段 update_fields_dict['gei_shangji_cishu'] = F('gei_shangji_cishu') + 1 update_fields_dict['gei_shangji_zonge'] = F('gei_shangji_zonge') + shangji_fenhong # 设置当天上级店铺ID(仅当天第一次结算时写入,后续保持不变) current_shangji_id = tongji.shangji_id if current_shangji_id is None: update_fields_dict['shangji_id'] = shangji_dianpu_id elif current_shangji_id != shangji_dianpu_id: logger.warning( f"店铺 {dianpu_id} 当天上级ID不一致,已有 {current_shangji_id}," f"新上级 {shangji_dianpu_id},保持原上级ID" ) # --- 更新上级店铺的“作为上级获得”统计(完全隔离保护) --- try: _update_shangji_gain_stat_within_tx(shangji_dianpu_id, shangji_fenhong) except Exception as e: logger.error( f"更新上级店铺 {shangji_dianpu_id} 统计失败,不影响本店: {e}" ) elif caozuo_leixing == 3: # 退款 update_fields_dict['tuikuan_zongliang'] = F('tuikuan_zongliang') + 1 update_fields_dict['tuikuan_zonge'] = F('tuikuan_zonge') + dingdan_jiage else: logger.warning(f"未知操作类型: {caozuo_leixing}") return # 原子更新本店统计字段 if update_fields_dict: for field, value in update_fields_dict.items(): setattr(tongji, field, value) tongji.save(update_fields=list(update_fields_dict.keys())) logger.info( f"店铺统计更新完成: dianpu={dianpu_id}, 操作={caozuo_leixing}, " f"金额={dingdan_jiage}, 店铺收益={dianpu_shouyi}, " f"上级ID={shangji_dianpu_id}, 上级分红={shangji_fenhong}" ) def _update_shangji_gain_stat_within_tx(shangji_dianpu_id, fenhong_jine): """ 在事务内更新上级店铺的“作为上级获得”统计及主表余额。 注意:本函数必须由处于同一事务中的 update_dianpu_daily_stat 调用, 并且调用方已获取必要的行锁,这里再次加锁不会死锁(按相同顺序加锁)。 """ today = date.today() # 获取或创建上级店铺的今日统计记录并加锁 tongji_shangji, _ = DianpuShouzhiMeiriTongji.objects.select_for_update().get_or_create( dianpu_id=shangji_dianpu_id, riqi=today, defaults={ 'nian': today.year, 'yue': today.month, 'ri': today.day, 'xiadan_zongliang': 0, 'xiadan_zonge': Decimal('0.00'), 'chengjiao_zongliang': 0, 'chengjiao_zonge': Decimal('0.00'), 'tuikuan_zongliang': 0, 'tuikuan_zonge': Decimal('0.00'), 'shouyi_zonge': Decimal('0.00'), 'shangji_fenhong_cishu': 0, 'shangji_fenhong_zonge': Decimal('0.00'), 'gei_shangji_cishu': 0, 'gei_shangji_zonge': Decimal('0.00'), 'shangji_id': None, } ) # 更新上级店铺的“作为上级获得”字段 tongji_shangji.shangji_fenhong_cishu = F('shangji_fenhong_cishu') + 1 tongji_shangji.shangji_fenhong_zonge = F('shangji_fenhong_zonge') + fenhong_jine tongji_shangji.save(update_fields=['shangji_fenhong_cishu', 'shangji_fenhong_zonge', 'update_time']) # 更新上级店铺主表的分红余额和累计分红 shangji = Dianpu.objects.select_for_update().get(id=shangji_dianpu_id) shangji.shangji_fenhong_leiji = F('shangji_fenhong_leiji') + fenhong_jine shangji.shangji_ketixian_fenhong = F('shangji_ketixian_fenhong') + fenhong_jine shangji.save(update_fields=['shangji_fenhong_leiji', 'shangji_ketixian_fenhong', 'update_time']) logger.info(f"上级店铺 {shangji_dianpu_id} 获得分红 +{fenhong_jine}") def calculate_pingtai_and_dianpu_shouyi(shangpin_id, dingdan_jiage, dashou_fencheng): """ 计算平台收益、店铺收益、上级分红 分配链路(上级分红完全隔离,不影响平台及店铺核心计算): 总收益 = 订单金额 - 打手分成 平台抽成 = 总收益 * (店铺开启则用店铺费率,否则用平台默认费率) 剩余收益 = 总收益 - 平台抽成 上级分红 = 剩余收益 * 本店设置的上级分红费率(若有上级且满足条件) 店铺收益 = 剩余收益 - 上级分红 参数: shangpin_id: 商品ID dingdan_jiage: 订单金额 dashou_fencheng: 打手分成金额 返回: 五元组 (dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong) - dianpu_id: 店铺ID,可能为None - pingtai_shouyi: 平台收益 - dianpu_shouyi: 店铺最终收益 - shangji_dianpu_id: 上级店铺ID,可能为None - shangji_fenhong: 上级分红金额,默认0.00 """ # ==================== 金额安全处理 ==================== dingdan_jiage = Decimal(str(dingdan_jiage)) if dingdan_jiage else Decimal('0.00') dashou_fencheng = Decimal(str(dashou_fencheng)) if dashou_fencheng else Decimal('0.00') if dingdan_jiage < 0: dingdan_jiage = Decimal('0.00') if dashou_fencheng < 0: dashou_fencheng = Decimal('0.00') zong_shouyi = dingdan_jiage - dashou_fencheng if zong_shouyi < 0: zong_shouyi = Decimal('0.00') # ==================== 获取商品及店铺 ==================== try: shangpin = Shangpin.objects.select_related('dianpu').only('id', 'dianpu').get(id=shangpin_id) except Shangpin.DoesNotExist: logger.error(f"收益计算失败:商品ID {shangpin_id} 不存在") return None, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') dianpu_id = shangpin.dianpu_id if dianpu_id is None: logger.info(f"商品 {shangpin_id} 未关联店铺,平台收益={zong_shouyi}") return None, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') # ==================== 获取店铺配置 ==================== try: dianpu = Dianpu.objects.only( 'id', 'kaiqi_shouyi_choucheng', 'shouyi_choucheng_feilv', 'yaoqing_dianpu_id', 'fenhong_choucheng_feilv' ).get(id=dianpu_id) except Dianpu.DoesNotExist: logger.error(f"收益计算失败:店铺ID {dianpu_id} 不存在") return dianpu_id, zong_shouyi, Decimal('0.00'), None, Decimal('0.00') # ==================== 平台抽成 ==================== if dianpu.kaiqi_shouyi_choucheng and dianpu.shouyi_choucheng_feilv is not None: feilv = dianpu.shouyi_choucheng_feilv else: moren_peizhi, created = DianpuMorenPeizhi.objects.get_or_create( id=1, defaults={ 'moren_choucheng_feilv': Decimal('0.1000'), 'moren_tixian_xiane': Decimal('1000.00') } ) feilv = moren_peizhi.moren_choucheng_feilv if feilv is None or feilv < 0: feilv = Decimal('0.0000') if feilv > 1: feilv = Decimal('1.0000') pingtai_shouyi = zong_shouyi * feilv shengyu = zong_shouyi - pingtai_shouyi # ==================== 上级分红(完全隔离,不影响店铺收益) ==================== shangji_dianpu_id = dianpu.yaoqing_dianpu_id shangji_fenhong = Decimal('0.00') if shangji_dianpu_id and dianpu.fenhong_choucheng_feilv: try: # 检查上级店铺状态 shangji = Dianpu.objects.filter(id=shangji_dianpu_id).only('id', 'zhuangtai').first() if shangji and shangji.zhuangtai == 1: fz_rate = dianpu.fenhong_choucheng_feilv if fz_rate and fz_rate > 0: shangji_fenhong = shengyu * fz_rate if shangji_fenhong < 0: shangji_fenhong = Decimal('0.00') if shangji_fenhong > shengyu: shangji_fenhong = shengyu shengyu -= shangji_fenhong else: if shangji: logger.info(f"上级店铺 {shangji_dianpu_id} 状态非正常,不参与分红") else: logger.warning(f"上级店铺ID {shangji_dianpu_id} 不存在,不参与分红") except Exception as e: logger.exception(f"上级分红计算异常,忽略: {e}") shangji_fenhong = Decimal('0.00') # 注意:shengyu 未被修改,店铺收益不变 dianpu_shouyi = shengyu logger.info( f"收益计算完成: 商品={shangpin_id}, 总收益={zong_shouyi}, 平台={pingtai_shouyi}, " f"上级ID={shangji_dianpu_id}, 上级分红={shangji_fenhong}, 店铺={dianpu_shouyi}" ) return dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong def validate_shangpin_and_dianpu(shangpin_id): """ 校验商品及关联店铺(区分有无店铺) 无店铺:仅校验商品是否上架 有店铺:校验商品审核、封禁、上架,以及店铺是否封禁 返回: (True, None, shangpin_obj) # 通过 (False, "错误信息", None) # 失败 """ try: shangpin = Shangpin.objects.select_related('dianpu').get(id=shangpin_id) except Shangpin.DoesNotExist: return False, "商品不存在", None # 无店铺:只检查上架 if not shangpin.dianpu_id: if not shangpin.shangjia_zhuangtai: return False, "商品已下架", None return True, None, shangpin # 有店铺:完整校验 if not shangpin.shenhe_zhuangtai: return False, "商品未通过平台审核", None if shangpin.fengjin_zhuangtai: return False, "商品已被封禁", None if not shangpin.shangjia_zhuangtai: return False, "商品已下架", None try: if shangpin.dianpu.zhuangtai != 1: return False, "店铺已被封禁", None except Exception: return False, "关联店铺异常", None return True, None, shangpin