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('订单无支付时间,不能售后'); } } /** * 原单是否可走隔天售后(即原「冲销」路径): * - 非当天支付(仅看 payTime);或 * - 挂账已结清;或 * - 已结账单(当天已结账也按冲销处理,不走普通售后) * @return array{ok:bool,reason:string,sameDay:int} */ public static function checkEligible($order) { if (empty($order)) { return ['ok' => false, 'reason' => '没有原订单', 'sameDay' => self::SAME_DAY_YES, 'hasPayTime' => 0]; } // 只用支付时间;无支付时间本单不能售后(由 assertCanRefund 拦截) if (!self::hasValidPayTime($order)) { return [ 'ok' => false, 'reason' => '订单无支付时间,不能售后', 'sameDay' => self::SAME_DAY_YES, 'hasPayTime' => 0, ]; } $payTime = $order->payTime; $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d'); $debtPrice = bcadd((string)($order->debtPrice ?? '0'), '0', 2); $remainDebt = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2); $debtCleared = bccomp($debtPrice, '0', 2) > 0 && bccomp($remainDebt, '0', 2) === 0; // 已结账:当天也强制走冲销(sameDay=0),与隔天同一套资金/金额逻辑 $orderCleared = !empty($order->clearId); if ($isToday && !$debtCleared && !$orderCleared) { return [ 'ok' => false, 'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/隔天可售后', 'sameDay' => self::SAME_DAY_YES, 'hasPayTime' => 1, ]; } return [ 'ok' => true, 'reason' => '', 'sameDay' => self::SAME_DAY_NO, 'suggestFundBalance' => true, 'orderCleared' => $orderCleared ? 1 : 0, 'hasPayTime' => 1, ]; } /** * 创建/审核时是否必须按隔天售后(sameDay=0): * 与 checkEligible 对齐——非当天 / 已结账单 / 挂账已结清,均不可再走当天售后。 * 无支付时间返回 false(须先走 assertCanRefund)。 */ public static function mustUseNextDay($order) { if (!self::hasValidPayTime($order)) { return false; } $check = self::checkEligible($order); return !empty($check['ok']); } /** * 原单是否为微信/支付宝线上支付(隔天也可原路退回的前提) */ public static function isOnlinePayOrder($order) { if (empty($order)) { return false; } $payWay = intval($order->payWay ?? 0); $onlinePay = intval($order->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 defaultNextDayFundType($order) { return self::isOnlinePayOrder($order) ? self::FUND_ORIGINAL : self::FUND_BALANCE; } /** * 当天售后未选手选资金时的默认值:线上原路;余额/挂账返余额;线下默认线下转账。 */ public static function defaultSameDayFundType($order) { if (self::isOnlinePayOrder($order)) { return self::FUND_ORIGINAL; } $payWay = intval($order->payWay ?? 0); $balancePay = dict::getDict('payWay', 'balancePay'); $debtPay = dict::getDict('payWay', 'debtPay'); if ($payWay === $balancePay || $payWay === $debtPay) { return self::FUND_BALANCE; } return self::FUND_NONE; } /** * 解析资金方式(当天/隔天通用):挂账/余额强制返余额;线上可选原路/返余额/线下转账。 * $hasRelateOrder=false 表示无原单退款,不可原路退。 */ public static function resolveFundType($fundType, $order, $hasRelateOrder = true) { $fundType = intval($fundType); // 无原单:只能返余额或线下自行转账 if (!$hasRelateOrder || empty($order)) { if ($fundType === self::FUND_ORIGINAL) { util::fail('无原单退款不能原路退回'); } if (!in_array($fundType, [self::FUND_BALANCE, self::FUND_NONE], true)) { return self::FUND_BALANCE; } return $fundType; } $payWay = intval($order->payWay ?? 0); $debtPay = dict::getDict('payWay', 'debtPay'); $balancePay = dict::getDict('payWay', 'balancePay'); // 挂账/余额付款:资金落到返充余额 if ($payWay === $debtPay || $payWay === $balancePay) { return self::FUND_BALANCE; } // 线上付款:可选原路 / 返余额 / 线下转账 if (self::isOnlinePayOrder($order)) { if (!in_array($fundType, [self::FUND_ORIGINAL, self::FUND_BALANCE, self::FUND_NONE], true)) { util::fail('请选择资金处理方式'); } return $fundType; } // 线下付款:返余额或线下自行转账 if (!in_array($fundType, [self::FUND_BALANCE, self::FUND_NONE], true)) { return self::FUND_BALANCE; } return $fundType; } /** * 生成写入售后单的资金快照:fundType + onlinePay + payWay(落 xhRefund/xhCgRefund)。 * onlinePay:原路且走线上通道为 2,否则 1;payWay:原路/线下转账的具体渠道。 * * @param int $fundTypeInput 前端/上游传入,0 表示按 sameDay 自动默认 * @param object|null $order 原销售单;无原单传 null * @param bool $hasRelateOrder 是否有关联原单 * @param array $options sameDay、payWay(线下转账选手选渠道) * @return array{fundType:int,onlinePay:int,payWay:int} */ public static function buildRefundFundSnapshot($fundTypeInput, $order = null, $hasRelateOrder = true, $options = []) { $fundTypeInput = intval($fundTypeInput); $sameDay = intval($options['sameDay'] ?? self::SAME_DAY_YES); $onlineNot = intval(dict::getDict('onlinePay', 'not')); $onlineYes = intval(dict::getDict('onlinePay', 'yes')); if ($fundTypeInput === self::FUND_UNUSED && $hasRelateOrder && !empty($order)) { $fundTypeInput = ($sameDay === self::SAME_DAY_NO) ? self::defaultNextDayFundType($order) : self::defaultSameDayFundType($order); } $fundType = self::resolveFundType($fundTypeInput, $order, $hasRelateOrder); // 返充余额:渠道固定为余额,非线上原路 if ($fundType === self::FUND_BALANCE) { return [ 'fundType' => self::FUND_BALANCE, 'onlinePay' => $onlineNot, 'payWay' => intval(dict::getDict('payWay', 'balancePay')), ]; } // 线下自行转账:优先用选手选 payWay(微信/支付宝/现金/银行卡) if ($fundType === self::FUND_NONE) { $payWay = self::normalizeOfflinePayWay($options['payWay'] ?? null, $order); return [ 'fundType' => self::FUND_NONE, 'onlinePay' => $onlineNot, 'payWay' => $payWay, ]; } // 原路退回:线上微信/支付宝标记 onlinePay=2;否则记线下原路渠道 if ($fundType === self::FUND_ORIGINAL && !empty($order)) { $orderPayWay = intval($order->payWay ?? 0); if (self::isOnlinePayOrder($order)) { return [ 'fundType' => self::FUND_ORIGINAL, 'onlinePay' => $onlineYes, 'payWay' => $orderPayWay, ]; } return [ 'fundType' => self::FUND_ORIGINAL, 'onlinePay' => $onlineNot, 'payWay' => $orderPayWay, ]; } return [ 'fundType' => self::FUND_UNUSED, 'onlinePay' => $onlineNot, 'payWay' => 0, ]; } /** * 线下转账渠道:允许 0微信/1支付宝/4现金/5银行卡;非法则回落原单或现金 */ 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')); } /** * GHS 侧隔天通过:累计 nextDayTkPrice,按 fundType 处理资金,不改 actPrice。 * @return string 返余额后的客户余额(非返余额则为当前余额) */ public static function applyGhsAmountAndFund($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']); $fundType = intval($refund->fundType ?? self::FUND_UNUSED); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); if ($fundType === self::FUND_BALANCE) { $pair = GhsRechargeSettleService::lockAccountPair($custom); CustomClass::nextDayRefundReturnBalance($pair['custom'], $pair['ghs'], $refund, $order); $custom = CustomClass::getById($customId, true); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); } // FUND_ORIGINAL:网关退款在采购侧执行;FUND_NONE:线下自行转账(只记账) return $balance; } /** * HD 采购侧隔天通过:累计 nextDayTkPrice;原路退走拉卡拉,不改 actPrice。 */ public static function applyCgAmountAndFund($cgRefund, $cg) { $refundPrice = bcadd((string)($cgRefund->refundPrice ?? '0'), '0', 2); $cg->nextDayTkPrice = bcadd((string)($cg->nextDayTkPrice ?? '0'), $refundPrice, 2); $cg->refund = PurchaseClass::REFUND_YES; $cg->save(false, ['nextDayTkPrice', 'refund']); $fundType = intval($cgRefund->fundType ?? self::FUND_UNUSED); if ($fundType === self::FUND_ORIGINAL) { CgRefundClass::nextDayOriginalOnlineRefund($cgRefund, $cg); } $ghsId = intval($cg->ghsId ?? 0); if ($ghsId > 0) { $ghs = \bizHd\ghs\classes\GhsClass::getLockById($ghsId); if (!empty($ghs) && isset($ghs->expendAmount)) { $expend = bcsub((string)($ghs->expendAmount ?? '0'), $refundPrice, 2); if (bccomp($expend, '0', 2) < 0) { $expend = '0.00'; } $ghs->expendAmount = $expend; $ghs->save(false, ['expendAmount']); } } } /** * 无原单退款通过:加库存(退货退款)、累计门店支出、扣客户消费、按 fundType 返余额/仅记账。 * 不写订单 nextDayTkPrice(无关联销售单)。 */ public static function passFreeRefund($refund) { if (empty($refund)) { util::fail('退款单不存在'); } if (intval($refund->status) === RefundOrderClass::STATUS_COMPLETE) { util::fail('已通过'); } if (intval($refund->status) === RefundOrderClass::STATUS_REJECT) { util::fail('已驳回'); } if (intval($refund->status) === RefundOrderClass::STATUS_CANCEL) { util::fail('已取消'); } $refund->status = RefundOrderClass::STATUS_COMPLETE; $refund->passTime = date('Y-m-d H:i:s'); $refund->save(); $refundSn = $refund->orderSn ?? ''; $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2); $refundType = intval($refund->refundType ?? RefundOrderClass::REFUND_TYPE_MONEY_GOOD); $shopId = intval($refund->shopId ?? 0); $mainId = intval($refund->mainId ?? 0); $sjId = intval($refund->sjId ?? 0); $customName = $refund->customName ?? ''; // 有明细则置通过;仅「退货退款」回库(库存不退只记账不改库存) $refundItemList = RefundOrderItemClass::getAllByCondition(['orderSn' => $refundSn], null, '*', null, true); if ($refundType === RefundOrderClass::REFUND_TYPE_MONEY_GOOD && empty($refundItemList)) { util::fail('没有找到退款花材'); } if (!empty($refundItemList)) { foreach ($refundItemList as $val) { $val->status = 1; $val->save(); if ($refundType !== RefundOrderClass::REFUND_TYPE_MONEY_GOOD) { continue; } $productId = intval($val->productId ?? 0); $ptItemId = intval($val->itemId ?? 0); $num = bcadd((string)($val->xhNum ?? '0'), '0', 2); if (bccomp($num, '0', 2) <= 0 || $productId <= 0) { continue; } if (intval($val->xhUnitType ?? 0) === 1) { $smallNum = $num; $bigNum = 0; } else { $smallNum = 0; $bigNum = $num; } $stockInfo = ProductClass::addStock($productId, $bigNum, $smallNum); ProductClass::tryRecoverLimitBuyWhenStockRollbackFromZero($productId, $stockInfo['oldStock'] ?? 0); $oldStock = $stockInfo['oldStock'] ?? 0; $newStock = $stockInfo['newStock'] ?? 0; $recordData = [ 'relateName' => $customName, 'itemNum' => $val->itemNum ?? 0, 'sjId' => $sjId, 'shopId' => $shopId, 'mainId' => $mainId, 'orderSn' => $refundSn, 'itemId' => $ptItemId, 'oldStock' => $oldStock, 'productId' => $productId, 'newStock' => $newStock, 'io' => 1, ]; StockRecordClass::ghsRefundAddRecord($recordData); $val->itemStock = $oldStock; $val->newStock = $newStock; $val->save(); } } $main = MainClass::getLockById($mainId); if (empty($main)) { util::fail('没有main信息'); } $main->totalExpend = bcadd((string)($main->totalExpend ?? '0'), $refundPrice, 2); $main->totalRefund = bcadd((string)($main->totalRefund ?? '0'), $refundPrice, 2); $main->save(); return self::applyFreeFund($refund); } /** * 无原单退款资金:扣减客户累计消费;返余额走成对账户,仅记账不动余额。 * @return string 处理后的客户余额 */ 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']); $fundType = intval($refund->fundType ?? self::FUND_UNUSED); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); if ($fundType === self::FUND_BALANCE) { $pair = GhsRechargeSettleService::lockAccountPair($custom); // 无原单:order 传 null,流水挂售后单号 CustomClass::nextDayRefundReturnBalance($pair['custom'], $pair['ghs'], $refund, null); $custom = CustomClass::getById($customId, true); $balance = bcadd((string)($custom->balance ?? '0'), '0', 2); } return $balance; } /** * 按时段汇总成功隔天售后金额(收入/销量统计扣减用) */ public static function sumAmountByMainAndTime($mainId, $startTime, $endTime) { $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhRefund WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay AND passTime BETWEEN :start AND :end"; $row = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => RefundOrderClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryOne(); return bcadd($row['total'] ?? '0', '0', 2); } /** * 渠道收入对冲:拉取隔天成功售后及原单支付字段 */ public static function listForChannelIncomeDeduct($mainId, $startTime, $endTime) { $sql = "SELECT r.refundPrice AS amount, r.fundType, o.id AS orderId, o.debtPrice, o.remainDebtPrice, o.onlinePay, o.payWay AS orderPayWay, o.payWay FROM xhRefund r LEFT JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn 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' => RefundOrderClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); return $rows ?: []; } /** * 按客户汇总隔天退款金额 */ public static function sumAmountGroupByCustom($mainId, $startTime, $endTime) { $sql = "SELECT r.customId, COALESCE(SUM(r.refundPrice),0) AS total FROM xhRefund 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' => RefundOrderClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':start' => $startTime, ':end' => $endTime, ])->queryAll(); $map = []; foreach ($rows as $row) { $map[$row['customId']] = bcadd($row['total'] ?? '0', '0', 2); } return $map; } /** * 按花材汇总隔天退货数量与金额(退货退款明细) */ public static function sumItemByProduct($mainId, $startTime, $endTime) { $sql = "SELECT i.productId, COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount FROM xhRefundItem i INNER JOIN xhRefund r ON r.orderSn = i.orderSn 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.productId"; $rows = Yii::$app->db->createCommand($sql, [ ':mainId' => $mainId, ':status' => RefundOrderClass::STATUS_COMPLETE, ':sameDay' => self::SAME_DAY_NO, ':rtype' => RefundOrderClass::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; } }