payTime ?? ''; return !empty($payTime) && $payTime !== '0000-00-00 00:00:00'; } /** * 无支付时间不能售后 */ public static function assertCanRefund($order) { if (empty($order)) { util::fail('没有原订单'); } if (!self::hasValidPayTime($order)) { util::fail('订单无支付时间,不能售后'); } } /** * 是否必须按跨天售后(sameDay=0): * 非付款日 / 挂账已结清 / 已过可售后时限 * @param object $order * @param object|null $main */ public static function mustUseNextDay($order, $main = null) { $check = self::checkEligible($order, $main); return !empty($check['ok']); } /** * 原单是否可走跨天售后(售后页预检,对齐 ghs NextDayRefundClass::checkEligible) * @return array{ok:bool,reason:string,sameDay:int,orderCleared:int,hasPayTime:int} */ public static function checkEligible($order, $main = null) { if (empty($order)) { return [ 'ok' => false, 'reason' => '没有原订单', 'sameDay' => self::SAME_DAY_YES, 'orderCleared' => 0, 'hasPayTime' => 0, ]; } if (!self::hasValidPayTime($order)) { return [ 'ok' => false, 'reason' => '订单无支付时间,不能售后', 'sameDay' => self::SAME_DAY_YES, 'orderCleared' => 0, 'hasPayTime' => 0, ]; } $payTime = $order->payTime; $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d'); $debtPay = intval(dict::getDict('payWay', 'debtPay')); $debtCleared = false; if (intval($order->payWay ?? 0) === $debtPay) { $debtCleared = bccomp((string)($order->remainDebtPrice ?? '0'), '0', 2) <= 0; } $orderCleared = !empty($order->clearId); $overTimeLimit = false; if (!empty($main)) { $cRet = OrderClass::couldRefund($order, $main); if (empty($cRet['could'])) { $overTimeLimit = true; } } // 当天且未结清、未超时 → 普通当天售后 if ($isToday && !$debtCleared && !$orderCleared && !$overTimeLimit) { return [ 'ok' => false, 'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/超时/隔天可售后', 'sameDay' => self::SAME_DAY_YES, 'orderCleared' => 0, 'hasPayTime' => 1, ]; } return [ 'ok' => true, 'reason' => '', 'sameDay' => self::SAME_DAY_NO, 'orderCleared' => ($orderCleared || $debtCleared) ? 1 : 0, 'hasPayTime' => 1, ]; } /** * 是否微信/支付宝线上支付 */ public static function isOnlinePayOrder($orderOrRefund) { if (empty($orderOrRefund)) { return false; } $payWay = intval($orderOrRefund->payWay ?? 0); $onlinePay = intval($orderOrRefund->onlinePay ?? dict::getDict('onlinePay', 'not')); $wxPay = dict::getDict('payWay', 'wxPay'); $aliPay = dict::getDict('payWay', 'alipay'); return $onlinePay == dict::getDict('onlinePay', 'yes') && in_array($payWay, [$wxPay, $aliPay], true); } /** * 余额付或挂账:原路就是动余额,禁止再「返充」 */ public static function isBalanceOrDebtPayWay($payWay) { $payWay = intval($payWay); return $payWay === intval(dict::getDict('payWay', 'balancePay')) || $payWay === intval(dict::getDict('payWay', 'debtPay')); } /** * 是否待选手选资金落地 */ public static function needFundAction($refund) { if (empty($refund)) { return 0; } if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) { return 0; } if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) { return 0; } if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) { return 0; } return 1; } /** * 生成售后资金快照(落 xhHdRefund) * @param object|null $order * @param bool $hasRelateOrder * @param array $options * @return array */ public static function buildRefundPaySnapshot($order = null, $hasRelateOrder = true, $options = []) { $onlineNot = intval(dict::getDict('onlinePay', 'not')); if (!$hasRelateOrder || empty($order)) { $payWay = self::normalizeOfflinePayWay($options['payWay'] ?? null, null); return [ 'onlinePay' => $onlineNot, 'payWay' => $payWay, 'hasReturn' => self::FLAG_NO, 'couldReturn' => self::FLAG_YES, 'returnBalance' => self::FLAG_NO, ]; } $payWay = intval($order->payWay ?? 0); $onlinePay = intval($order->onlinePay ?? $onlineNot); $couldReturn = self::isBalanceOrDebtPayWay($payWay) ? self::FLAG_NO : self::FLAG_YES; return [ 'onlinePay' => $onlinePay, 'payWay' => $payWay, 'hasReturn' => self::FLAG_NO, 'couldReturn' => $couldReturn, 'returnBalance' => self::FLAG_NO, ]; } /** * 线下转账渠道归一 */ protected static function normalizeOfflinePayWay($payWay, $order = null) { $allowed = [ intval(dict::getDict('payWay', 'wxPay')), intval(dict::getDict('payWay', 'alipay')), intval(dict::getDict('payWay', 'cash')), intval(dict::getDict('payWay', 'bankCard')), ]; if ($payWay !== null && $payWay !== '' && in_array(intval($payWay), $allowed, true)) { return intval($payWay); } if (!empty($order)) { $orderPayWay = intval($order->payWay ?? -1); if (in_array($orderPayWay, $allowed, true)) { return $orderPayWay; } } return intval(dict::getDict('payWay', 'cash')); } /** * 同步资金三态字段 */ public static function syncRefundFundFlags($refund, $fields = []) { if (empty($refund) || empty($fields)) { return; } foreach ($fields as $k => $v) { $refund->$k = $v; } $refund->save(false, array_keys($fields)); } /** * 标记已线下原路退回 */ public static function markHasReturn($refund) { if (empty($refund)) { util::fail('退款单不存在'); } if (intval($refund->status) !== HdRefundClass::STATUS_COMPLETE) { util::fail('售后未通过,不能确认原路退回'); } if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) { util::fail('已返充余额,不能再标记原路退回'); } if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) { return $refund; } self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]); return HdRefundClass::getById($refund->id, true); } /** * 不想线下原路:返充到客户余额 * @return array{refund:object,customBalance:string} */ public static function applyReturnBalance($refund, $order = null) { if (empty($refund)) { util::fail('退款单不存在'); } if (intval($refund->status) !== HdRefundClass::STATUS_COMPLETE) { util::fail('售后未通过,不能返充余额'); } if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) { util::fail('已原路退回,不能再返充余额'); } if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) { util::fail('已返充到余额'); } if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) { util::fail('该单不允许返充到余额'); } $customId = intval($refund->customId ?? 0); $custom = CustomClass::getLockById($customId); if (empty($custom)) { util::fail('没有找到客户'); } $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2); CustomClass::nextDayRefundReturnBalance($custom, $refundPrice, $refund, $order); self::syncRefundFundFlags($refund, ['returnBalance' => self::FLAG_YES]); $custom = CustomClass::getById($customId, true); return [ 'refund' => HdRefundClass::getById($refund->id, true), 'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2), ]; } /** * 有原单隔天通过:累计 nextDayTkPrice;余额/挂账原路加余额并 hasReturn=1 * @return string 当前客户余额 */ public static function applyHdAmountAndFund($refund, $order) { $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2); $order->nextDayTkPrice = bcadd((string)($order->nextDayTkPrice ?? '0'), $refundPrice, 2); $order->refund = OrderClass::REFUND_YES; $order->save(false, ['nextDayTkPrice', 'refund']); $customId = intval($order->customId ?? 0); $custom = CustomClass::getLockById($customId); if (empty($custom)) { util::fail('没有找到客户'); } $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2); if (bccomp($buyAmount, '0', 2) < 0) { $buyAmount = '0.00'; } $custom->buyAmount = $buyAmount; $custom->save(false, ['buyAmount']); $hdId = intval($custom->hdId ?? 0); if ($hdId > 0) { $hd = HdClass::getLockById($hdId); if (!empty($hd) && isset($hd->expendAmount)) { $expend = bcsub((string)($hd->expendAmount ?? '0'), $refundPrice, 2); if (bccomp($expend, '0', 2) < 0) { $expend = '0.00'; } $hd->expendAmount = $expend; $hd->save(false, ['expendAmount']); } } $payWay = intval($refund->payWay ?? ($order->payWay ?? 0)); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); // 余额/挂账:原路加回余额,完成后 hasReturn=1 if (self::isBalanceOrDebtPayWay($payWay)) { CustomClass::skRefundDebtAmountReduce($custom, $refundPrice, $refund, $payWay); self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]); $custom = CustomClass::getById($customId, true); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); } elseif (self::isOnlinePayOrder($refund) || self::isOnlinePayOrder($order)) { // 线上原路:沿用当天售后拉卡拉通道(由调用方 HdRefundService 处理更合适) // 此处只记账金额;线上退款在 service 里执行后置 hasReturn } return $balance; } /** * 门店支出流水(无原单 / 隔天共用) */ public static function addMainExpendForRefund($mainId, $shopId, $sjId, $refundPrice, $event = '销售退款') { $main = MainClass::getLockById($mainId); if (empty($main)) { util::fail('main信息缺失'); } $shop = ShopClass::getLockById($shopId); if (empty($shop)) { util::fail('没有找到门店'); } $currentTotalExpend = bcadd((string)($main->totalExpend ?? '0'), $refundPrice, 2); $main->totalExpend = $currentTotalExpend; $main->totalRefund = bcadd((string)($main->totalRefund ?? '0'), $refundPrice, 2); $main->save(); $thisType = dict::getDict('capitalType', 'hdOrderRefund', 'id'); ShopCapitalClass::addCapital([ 'capitalType' => $thisType, 'io' => 0, 'totalExpend' => $currentTotalExpend, 'payWay' => 0, 'amount' => $refundPrice, 'sjId' => $sjId, 'shopId' => $shopId, 'event' => $event, 'mainId' => $mainId, ]); return $main; } /** * 无原单退款:扣客户累计消费;资金留给成功页确认 */ public static function applyFreeFund($refund) { $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2); $customId = intval($refund->customId ?? 0); $custom = CustomClass::getLockById($customId); if (empty($custom)) { util::fail('没有找到客户'); } $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2); if (bccomp($buyAmount, '0', 2) < 0) { $buyAmount = '0.00'; } // $custom->buyAmount = $buyAmount; // $custom->save(false, ['buyAmount']); $hdId = intval($custom->hdId ?? 0); if ($hdId > 0) { $hd = HdClass::getLockById($hdId); if (!empty($hd) && isset($hd->expendAmount)) { $expend = bcsub((string)($hd->expendAmount ?? '0'), $refundPrice, 2); if (bccomp($expend, '0', 2) < 0) { $expend = '0.00'; } $hd->expendAmount = $expend; $hd->save(false, ['expendAmount']); } } return bcadd((string)($custom->balance ?? '0'), '0', 2); } /** * 组装成功页返回字段 */ public static function buildFundResultPayload($refund, $custom = null, $extra = []) { $customId = intval($refund->customId ?? 0); if (empty($custom) && $customId > 0) { $custom = CustomClass::getById($customId, true); } return array_merge([ 'id' => intval($refund->id ?? 0), 'orderSn' => $refund->refundSn ?? ($refund->orderSn ?? ''), 'refundSn' => $refund->refundSn ?? '', 'sameDay' => intval($refund->sameDay ?? self::SAME_DAY_YES), 'onlinePay' => intval($refund->onlinePay ?? 0), 'payWay' => intval($refund->payWay ?? 0), 'hasReturn' => intval($refund->hasReturn ?? 0), 'couldReturn' => intval($refund->couldReturn ?? 0), 'returnBalance' => intval($refund->returnBalance ?? 0), 'refundPrice' => $refund->refundPrice ?? '0', 'customId' => $customId, 'customName' => $refund->customName ?? ($custom->name ?? ''), 'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2), 'orderId' => intval($refund->orderId ?? 0), 'needFundAction' => self::needFundAction($refund), ], $extra); } /** * 按时段汇总成功跨天售后金额(收入统计扣减用,对齐 ghs NextDayRefundClass) */ public static function sumAmountByMainAndTime($mainId, $startTime, $endTime) { $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhHdRefund WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end"; $row = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryOne(); return bcadd($row['total'] ?? '0', '0', 2); } /** * 渠道收入对冲:拉取 HD 隔天成功售后及原单支付字段(扣零售 ls 渠道) * @return array */ public static function listForChannelIncomeDeduct($mainId, $startTime, $endTime) { $sql = "SELECT r.refundPrice AS amount, r.hasReturn, r.returnBalance, r.couldReturn, r.onlinePay AS refundOnlinePay, r.payWay AS refundPayWay, IFNULL(o.id, r.orderId) AS orderId, o.debtPrice, o.remainDebtPrice, o.onlinePay, o.payWay AS orderPayWay, o.payWay FROM xhHdRefund r LEFT JOIN xhOrder o ON o.orderSn = r.orderSn AND IFNULL(r.orderSn,'') <> '' WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); return $rows ?: []; } /** * 客户业绩列表用的稳定 key,与批发 xhGhsCustom.id 隔离,避免撞号串户 */ public static function customStatKey($customId) { return 'hd_' . intval($customId); } /** * 客户业绩展示名:统一加「零售-」前缀,便于与批发客户区分 */ public static function displayCustomName($name, $customId = 0) { $name = trim((string)$name); if ($name === '') { $name = intval($customId) > 0 ? ('客户' . intval($customId)) : '零售客户'; } // 已带前缀则不再重复 if (mb_strpos($name, '零售-') === 0) { return $name; } return '零售-' . $name; } /** * 补全售后单上缺失的客户名(优先退款单 customName,再查 xhCustom) * @param array $rows [['customId'=>, 'customName'=>], ...] * @return array customId => ['name'=>, 'py'=>] */ protected static function resolveCustomNameMap($rows) { $map = []; $needIds = []; foreach ($rows as $row) { $cid = intval($row['customId'] ?? 0); if ($cid <= 0) { continue; } $name = trim((string)($row['customName'] ?? '')); if ($name !== '') { $map[$cid] = ['name' => $name, 'py' => '']; } else { $needIds[$cid] = 1; } } if (!empty($needIds)) { $customs = CustomClass::getAllByCondition( ['id' => ['in', array_keys($needIds)]], null, 'id,name,py', 'id' ); foreach ($needIds as $cid => $_) { if (!empty($customs[$cid])) { $map[$cid] = [ 'name' => $customs[$cid]['name'] ?? '', 'py' => $customs[$cid]['py'] ?? '', ]; } } } return $map; } /** * 按客户汇总隔天退款金额(客户业绩等) * @return array customId => ['amount'=>string, 'customName'=>string, 'py'=>string] */ public static function sumAmountGroupByCustom($mainId, $startTime, $endTime) { $sql = "SELECT r.customId, COALESCE(SUM(r.refundPrice),0) AS total, MAX(NULLIF(r.customName,'')) AS customName FROM xhHdRefund r WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end GROUP BY r.customId"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); $nameMap = self::resolveCustomNameMap($rows ?: []); $map = []; foreach ($rows ?: [] as $row) { $cid = intval($row['customId'] ?? 0); $info = $nameMap[$cid] ?? ['name' => '', 'py' => '']; $map[$cid] = [ 'amount' => bcadd($row['total'] ?? '0', '0', 2), 'customName' => $info['name'] ?? '', 'py' => $info['py'] ?? '', ]; } return $map; } /** * 按客户汇总隔天退货数量(退货退款) * @return array customId => ['num'=>string, 'customName'=>string, 'py'=>string] */ public static function sumItemQtyByCustom($mainId, $startTime, $endTime) { $sql = "SELECT r.customId, COALESCE(SUM(i.num),0) AS num, MAX(NULLIF(r.customName,'')) AS customName FROM xhHdRefundItem i INNER JOIN xhHdRefund r ON r.refundSn = i.refundSn WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay AND r.refundType=:rtype AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end GROUP BY r.customId"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':rtype' => HdRefundClass::REFUND_TYPE_MONEY_GOOD, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); $nameMap = self::resolveCustomNameMap($rows ?: []); $map = []; foreach ($rows ?: [] as $row) { $cid = intval($row['customId'] ?? 0); $info = $nameMap[$cid] ?? ['name' => '', 'py' => '']; $map[$cid] = [ 'num' => bcadd($row['num'] ?? '0', '0', 2), 'customName' => $info['name'] ?? '', 'py' => $info['py'] ?? '', ]; } return $map; } /** * 按花材汇总隔天退货数量与金额(库存/分类收入统计) * @return array itemId => [num, amount] */ public static function sumItemByProduct($mainId, $startTime, $endTime) { $sql = "SELECT i.itemId AS productId, COALESCE(SUM(i.num),0) AS num, COALESCE(SUM(i.price),0) AS amount FROM xhHdRefundItem i INNER JOIN xhHdRefund r ON r.refundSn = i.refundSn WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay AND r.refundType=:rtype AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end GROUP BY i.itemId"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':rtype' => HdRefundClass::REFUND_TYPE_MONEY_GOOD, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); $map = []; foreach ($rows as $row) { $map[$row['productId']] = [ 'num' => bcadd($row['num'] ?? '0', '0', 2), 'amount' => bcadd($row['amount'] ?? '0', '0', 2), ]; } return $map; } /** * 隔天「仅退款」按通过日汇总(销售仅退款) */ public static function sumMoneyOnlyAmountByMainAndTime($mainId, $startTime, $endTime) { $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhHdRefund WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay AND refundType=:rtype AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end"; $row = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':rtype' => HdRefundClass::REFUND_TYPE_MONEY, ':start' => $startTime, ':end' => $endTime, ])->queryOne(); return bcadd($row['total'] ?? '0', '0', 2); } /** * 隔天「仅退款」按通过日分摊到原单花材(花材销量页:只扣金额不扣数量) * @return array productId => [num=>0, amount=>...] */ public static function sumMoneyOnlyAmountByProduct($mainId, $startTime, $endTime) { $sql = "SELECT r.id, r.refundPrice, r.orderSn FROM xhHdRefund r WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay AND r.refundType=:rtype AND IFNULL(NULLIF(r.orderSn,''), '') <> '' AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => HdRefundClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':rtype' => HdRefundClass::REFUND_TYPE_MONEY, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); if (empty($rows)) { return []; } $map = []; foreach ($rows as $row) { $refundPrice = bcadd((string)($row['refundPrice'] ?? '0'), '0', 2); if (bccomp($refundPrice, '0', 2) <= 0) { continue; } $orderSn = $row['orderSn'] ?? ''; $items = \bizHd\order\classes\OrderItemClass::getAllByCondition( ['orderSn' => $orderSn], null, 'itemId,num,unitPrice', null, true ); if (empty($items)) { continue; } $weights = []; $weightSum = '0.00'; foreach ($items as $item) { $pid = intval($item->itemId ?? 0); if ($pid <= 0) { continue; } $line = bcmul((string)($item->num ?? '0'), (string)($item->unitPrice ?? '0'), 2); if (bccomp($line, '0', 2) <= 0) { continue; } $weights[$pid] = bcadd($weights[$pid] ?? '0', $line, 2); $weightSum = bcadd($weightSum, $line, 2); } if (bccomp($weightSum, '0', 2) <= 0) { continue; } $allocated = '0.00'; $pids = array_keys($weights); $last = count($pids) - 1; foreach ($pids as $idx => $pid) { if ($idx === $last) { $part = bcsub($refundPrice, $allocated, 2); } else { $part = bcdiv(bcmul($refundPrice, $weights[$pid], 4), $weightSum, 2); $allocated = bcadd($allocated, $part, 2); } if (!isset($map[$pid])) { $map[$pid] = ['num' => '0.00', 'amount' => '0.00']; } $map[$pid]['amount'] = bcadd($map[$pid]['amount'], $part, 2); } } return $map; } /** * 支付日净销量:当天已退 = refundNum - nextRefundNum(与 ghs 同口径,可复用) */ public static function payDayRemainNum($xhNum, $refundNum, $nextRefundNum = 0) { $sameDayRefund = bcsub((string)$refundNum, (string)($nextRefundNum ?? 0), 2); if (bccomp($sameDayRefund, '0', 2) < 0) { $sameDayRefund = '0'; } return bcsub((string)$xhNum, $sameDayRefund, 2); } }