HdNextDayRefundClass.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. <?php
  2. /**
  3. * HD 零售跨天售后 / 无原单退款业务类
  4. * 用途:sameDay(当天/隔天)判定、支付快照与资金三态、隔天金额写入、无原单通过与返充;
  5. * 以及收入/销量统计用的隔天扣减查询(对齐 ghs NextDayRefundClass)。
  6. * 说明:sameDay 只区分当天/隔天,有无原单看 orderId;无原单常复用 sameDay=0 资金路径。
  7. * 调用方:HdRefundService、RefundController、StatKdClass、StatSaleClass。
  8. */
  9. namespace bizHd\refund\classes;
  10. use biz\shop\classes\MainClass;
  11. use biz\shop\classes\ShopCapitalClass;
  12. use bizHd\custom\classes\CustomClass;
  13. use bizHd\custom\classes\HdClass;
  14. use bizHd\merchant\classes\ShopClass;
  15. use bizHd\order\classes\OrderClass;
  16. use common\components\dict;
  17. use common\components\util;
  18. use Yii;
  19. class HdNextDayRefundClass
  20. {
  21. const SAME_DAY_YES = 1;
  22. const SAME_DAY_NO = 0;
  23. const FLAG_NO = 0;
  24. const FLAG_YES = 1;
  25. /**
  26. * 是否有有效支付时间
  27. */
  28. public static function hasValidPayTime($order)
  29. {
  30. if (empty($order)) {
  31. return false;
  32. }
  33. $payTime = $order->payTime ?? '';
  34. return !empty($payTime) && $payTime !== '0000-00-00 00:00:00';
  35. }
  36. /**
  37. * 无支付时间不能售后
  38. */
  39. public static function assertCanRefund($order)
  40. {
  41. if (empty($order)) {
  42. util::fail('没有原订单');
  43. }
  44. if (!self::hasValidPayTime($order)) {
  45. util::fail('订单无支付时间,不能售后');
  46. }
  47. }
  48. /**
  49. * 是否必须按跨天售后(sameDay=0):
  50. * 非付款日 / 挂账已结清 / 已过可售后时限
  51. * @param object $order
  52. * @param object|null $main
  53. */
  54. public static function mustUseNextDay($order, $main = null)
  55. {
  56. $check = self::checkEligible($order, $main);
  57. return !empty($check['ok']);
  58. }
  59. /**
  60. * 原单是否可走跨天售后(售后页预检,对齐 ghs NextDayRefundClass::checkEligible)
  61. * @return array{ok:bool,reason:string,sameDay:int,orderCleared:int,hasPayTime:int}
  62. */
  63. public static function checkEligible($order, $main = null)
  64. {
  65. if (empty($order)) {
  66. return [
  67. 'ok' => false,
  68. 'reason' => '没有原订单',
  69. 'sameDay' => self::SAME_DAY_YES,
  70. 'orderCleared' => 0,
  71. 'hasPayTime' => 0,
  72. ];
  73. }
  74. if (!self::hasValidPayTime($order)) {
  75. return [
  76. 'ok' => false,
  77. 'reason' => '订单无支付时间,不能售后',
  78. 'sameDay' => self::SAME_DAY_YES,
  79. 'orderCleared' => 0,
  80. 'hasPayTime' => 0,
  81. ];
  82. }
  83. $payTime = $order->payTime;
  84. $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d');
  85. $debtPay = intval(dict::getDict('payWay', 'debtPay'));
  86. $debtCleared = false;
  87. if (intval($order->payWay ?? 0) === $debtPay) {
  88. $debtCleared = bccomp((string)($order->remainDebtPrice ?? '0'), '0', 2) <= 0;
  89. }
  90. $orderCleared = !empty($order->clearId);
  91. $overTimeLimit = false;
  92. if (!empty($main)) {
  93. $cRet = OrderClass::couldRefund($order, $main);
  94. if (empty($cRet['could'])) {
  95. $overTimeLimit = true;
  96. }
  97. }
  98. // 当天且未结清、未超时 → 普通当天售后
  99. if ($isToday && !$debtCleared && !$orderCleared && !$overTimeLimit) {
  100. return [
  101. 'ok' => false,
  102. 'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/超时/隔天可售后',
  103. 'sameDay' => self::SAME_DAY_YES,
  104. 'orderCleared' => 0,
  105. 'hasPayTime' => 1,
  106. ];
  107. }
  108. return [
  109. 'ok' => true,
  110. 'reason' => '',
  111. 'sameDay' => self::SAME_DAY_NO,
  112. 'orderCleared' => ($orderCleared || $debtCleared) ? 1 : 0,
  113. 'hasPayTime' => 1,
  114. ];
  115. }
  116. /**
  117. * 是否微信/支付宝线上支付
  118. */
  119. public static function isOnlinePayOrder($orderOrRefund)
  120. {
  121. if (empty($orderOrRefund)) {
  122. return false;
  123. }
  124. $payWay = intval($orderOrRefund->payWay ?? 0);
  125. $onlinePay = intval($orderOrRefund->onlinePay ?? dict::getDict('onlinePay', 'not'));
  126. $wxPay = dict::getDict('payWay', 'wxPay');
  127. $aliPay = dict::getDict('payWay', 'alipay');
  128. return $onlinePay == dict::getDict('onlinePay', 'yes')
  129. && in_array($payWay, [$wxPay, $aliPay], true);
  130. }
  131. /**
  132. * 余额付或挂账:原路就是动余额,禁止再「返充」
  133. */
  134. public static function isBalanceOrDebtPayWay($payWay)
  135. {
  136. $payWay = intval($payWay);
  137. return $payWay === intval(dict::getDict('payWay', 'balancePay'))
  138. || $payWay === intval(dict::getDict('payWay', 'debtPay'));
  139. }
  140. /**
  141. * 是否待选手选资金落地
  142. */
  143. public static function needFundAction($refund)
  144. {
  145. if (empty($refund)) {
  146. return 0;
  147. }
  148. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  149. return 0;
  150. }
  151. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  152. return 0;
  153. }
  154. if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) {
  155. return 0;
  156. }
  157. return 1;
  158. }
  159. /**
  160. * 生成售后资金快照(落 xhHdRefund)
  161. * @param object|null $order
  162. * @param bool $hasRelateOrder
  163. * @param array $options
  164. * @return array
  165. */
  166. public static function buildRefundPaySnapshot($order = null, $hasRelateOrder = true, $options = [])
  167. {
  168. $onlineNot = intval(dict::getDict('onlinePay', 'not'));
  169. if (!$hasRelateOrder || empty($order)) {
  170. $payWay = self::normalizeOfflinePayWay($options['payWay'] ?? null, null);
  171. return [
  172. 'onlinePay' => $onlineNot,
  173. 'payWay' => $payWay,
  174. 'hasReturn' => self::FLAG_NO,
  175. 'couldReturn' => self::FLAG_YES,
  176. 'returnBalance' => self::FLAG_NO,
  177. ];
  178. }
  179. $payWay = intval($order->payWay ?? 0);
  180. $onlinePay = intval($order->onlinePay ?? $onlineNot);
  181. $couldReturn = self::isBalanceOrDebtPayWay($payWay) ? self::FLAG_NO : self::FLAG_YES;
  182. return [
  183. 'onlinePay' => $onlinePay,
  184. 'payWay' => $payWay,
  185. 'hasReturn' => self::FLAG_NO,
  186. 'couldReturn' => $couldReturn,
  187. 'returnBalance' => self::FLAG_NO,
  188. ];
  189. }
  190. /**
  191. * 线下转账渠道归一
  192. */
  193. protected static function normalizeOfflinePayWay($payWay, $order = null)
  194. {
  195. $allowed = [
  196. intval(dict::getDict('payWay', 'wxPay')),
  197. intval(dict::getDict('payWay', 'alipay')),
  198. intval(dict::getDict('payWay', 'cash')),
  199. intval(dict::getDict('payWay', 'bankCard')),
  200. ];
  201. if ($payWay !== null && $payWay !== '' && in_array(intval($payWay), $allowed, true)) {
  202. return intval($payWay);
  203. }
  204. if (!empty($order)) {
  205. $orderPayWay = intval($order->payWay ?? -1);
  206. if (in_array($orderPayWay, $allowed, true)) {
  207. return $orderPayWay;
  208. }
  209. }
  210. return intval(dict::getDict('payWay', 'cash'));
  211. }
  212. /**
  213. * 同步资金三态字段
  214. */
  215. public static function syncRefundFundFlags($refund, $fields = [])
  216. {
  217. if (empty($refund) || empty($fields)) {
  218. return;
  219. }
  220. foreach ($fields as $k => $v) {
  221. $refund->$k = $v;
  222. }
  223. $refund->save(false, array_keys($fields));
  224. }
  225. /**
  226. * 标记已线下原路退回
  227. */
  228. public static function markHasReturn($refund)
  229. {
  230. if (empty($refund)) {
  231. util::fail('退款单不存在');
  232. }
  233. if (intval($refund->status) !== HdRefundClass::STATUS_COMPLETE) {
  234. util::fail('售后未通过,不能确认原路退回');
  235. }
  236. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  237. util::fail('已返充余额,不能再标记原路退回');
  238. }
  239. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  240. return $refund;
  241. }
  242. self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]);
  243. return HdRefundClass::getById($refund->id, true);
  244. }
  245. /**
  246. * 不想线下原路:返充到客户余额
  247. * @return array{refund:object,customBalance:string}
  248. */
  249. public static function applyReturnBalance($refund, $order = null)
  250. {
  251. if (empty($refund)) {
  252. util::fail('退款单不存在');
  253. }
  254. if (intval($refund->status) !== HdRefundClass::STATUS_COMPLETE) {
  255. util::fail('售后未通过,不能返充余额');
  256. }
  257. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  258. util::fail('已原路退回,不能再返充余额');
  259. }
  260. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  261. util::fail('已返充到余额');
  262. }
  263. if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) {
  264. util::fail('该单不允许返充到余额');
  265. }
  266. $customId = intval($refund->customId ?? 0);
  267. $custom = CustomClass::getLockById($customId);
  268. if (empty($custom)) {
  269. util::fail('没有找到客户');
  270. }
  271. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  272. CustomClass::nextDayRefundReturnBalance($custom, $refundPrice, $refund, $order);
  273. self::syncRefundFundFlags($refund, ['returnBalance' => self::FLAG_YES]);
  274. $custom = CustomClass::getById($customId, true);
  275. return [
  276. 'refund' => HdRefundClass::getById($refund->id, true),
  277. 'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2),
  278. ];
  279. }
  280. /**
  281. * 有原单隔天通过:累计 nextDayTkPrice;余额/挂账原路加余额并 hasReturn=1
  282. * @return string 当前客户余额
  283. */
  284. public static function applyHdAmountAndFund($refund, $order)
  285. {
  286. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  287. $order->nextDayTkPrice = bcadd((string)($order->nextDayTkPrice ?? '0'), $refundPrice, 2);
  288. $order->refund = OrderClass::REFUND_YES;
  289. $order->save(false, ['nextDayTkPrice', 'refund']);
  290. $customId = intval($order->customId ?? 0);
  291. $custom = CustomClass::getLockById($customId);
  292. if (empty($custom)) {
  293. util::fail('没有找到客户');
  294. }
  295. $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2);
  296. if (bccomp($buyAmount, '0', 2) < 0) {
  297. $buyAmount = '0.00';
  298. }
  299. $custom->buyAmount = $buyAmount;
  300. $custom->save(false, ['buyAmount']);
  301. $hdId = intval($custom->hdId ?? 0);
  302. if ($hdId > 0) {
  303. $hd = HdClass::getLockById($hdId);
  304. if (!empty($hd) && isset($hd->expendAmount)) {
  305. $expend = bcsub((string)($hd->expendAmount ?? '0'), $refundPrice, 2);
  306. if (bccomp($expend, '0', 2) < 0) {
  307. $expend = '0.00';
  308. }
  309. $hd->expendAmount = $expend;
  310. $hd->save(false, ['expendAmount']);
  311. }
  312. }
  313. $payWay = intval($refund->payWay ?? ($order->payWay ?? 0));
  314. $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
  315. // 余额/挂账:原路加回余额,完成后 hasReturn=1
  316. if (self::isBalanceOrDebtPayWay($payWay)) {
  317. CustomClass::skRefundDebtAmountReduce($custom, $refundPrice, $refund, $payWay);
  318. self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]);
  319. $custom = CustomClass::getById($customId, true);
  320. $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
  321. } elseif (self::isOnlinePayOrder($refund) || self::isOnlinePayOrder($order)) {
  322. // 线上原路:沿用当天售后拉卡拉通道(由调用方 HdRefundService 处理更合适)
  323. // 此处只记账金额;线上退款在 service 里执行后置 hasReturn
  324. }
  325. return $balance;
  326. }
  327. /**
  328. * 门店支出流水(无原单 / 隔天共用)
  329. */
  330. public static function addMainExpendForRefund($mainId, $shopId, $sjId, $refundPrice, $event = '销售退款')
  331. {
  332. $main = MainClass::getLockById($mainId);
  333. if (empty($main)) {
  334. util::fail('main信息缺失');
  335. }
  336. $shop = ShopClass::getLockById($shopId);
  337. if (empty($shop)) {
  338. util::fail('没有找到门店');
  339. }
  340. $currentTotalExpend = bcadd((string)($main->totalExpend ?? '0'), $refundPrice, 2);
  341. $main->totalExpend = $currentTotalExpend;
  342. $main->totalRefund = bcadd((string)($main->totalRefund ?? '0'), $refundPrice, 2);
  343. $main->save();
  344. $thisType = dict::getDict('capitalType', 'hdOrderRefund', 'id');
  345. ShopCapitalClass::addCapital([
  346. 'capitalType' => $thisType,
  347. 'io' => 0,
  348. 'totalExpend' => $currentTotalExpend,
  349. 'payWay' => 0,
  350. 'amount' => $refundPrice,
  351. 'sjId' => $sjId,
  352. 'shopId' => $shopId,
  353. 'event' => $event,
  354. 'mainId' => $mainId,
  355. ]);
  356. return $main;
  357. }
  358. /**
  359. * 无原单退款:扣客户累计消费;资金留给成功页确认
  360. */
  361. public static function applyFreeFund($refund)
  362. {
  363. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  364. $customId = intval($refund->customId ?? 0);
  365. $custom = CustomClass::getLockById($customId);
  366. if (empty($custom)) {
  367. util::fail('没有找到客户');
  368. }
  369. $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2);
  370. if (bccomp($buyAmount, '0', 2) < 0) {
  371. $buyAmount = '0.00';
  372. }
  373. // $custom->buyAmount = $buyAmount;
  374. // $custom->save(false, ['buyAmount']);
  375. $hdId = intval($custom->hdId ?? 0);
  376. if ($hdId > 0) {
  377. $hd = HdClass::getLockById($hdId);
  378. if (!empty($hd) && isset($hd->expendAmount)) {
  379. $expend = bcsub((string)($hd->expendAmount ?? '0'), $refundPrice, 2);
  380. if (bccomp($expend, '0', 2) < 0) {
  381. $expend = '0.00';
  382. }
  383. $hd->expendAmount = $expend;
  384. $hd->save(false, ['expendAmount']);
  385. }
  386. }
  387. return bcadd((string)($custom->balance ?? '0'), '0', 2);
  388. }
  389. /**
  390. * 组装成功页返回字段
  391. */
  392. public static function buildFundResultPayload($refund, $custom = null, $extra = [])
  393. {
  394. $customId = intval($refund->customId ?? 0);
  395. if (empty($custom) && $customId > 0) {
  396. $custom = CustomClass::getById($customId, true);
  397. }
  398. return array_merge([
  399. 'id' => intval($refund->id ?? 0),
  400. 'orderSn' => $refund->refundSn ?? ($refund->orderSn ?? ''),
  401. 'refundSn' => $refund->refundSn ?? '',
  402. 'sameDay' => intval($refund->sameDay ?? self::SAME_DAY_YES),
  403. 'onlinePay' => intval($refund->onlinePay ?? 0),
  404. 'payWay' => intval($refund->payWay ?? 0),
  405. 'hasReturn' => intval($refund->hasReturn ?? 0),
  406. 'couldReturn' => intval($refund->couldReturn ?? 0),
  407. 'returnBalance' => intval($refund->returnBalance ?? 0),
  408. 'refundPrice' => $refund->refundPrice ?? '0',
  409. 'customId' => $customId,
  410. 'customName' => $refund->customName ?? ($custom->name ?? ''),
  411. 'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2),
  412. 'orderId' => intval($refund->orderId ?? 0),
  413. 'needFundAction' => self::needFundAction($refund),
  414. ], $extra);
  415. }
  416. /**
  417. * 按时段汇总成功跨天售后金额(收入统计扣减用,对齐 ghs NextDayRefundClass)
  418. */
  419. public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
  420. {
  421. $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhHdRefund
  422. WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
  423. AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end";
  424. $row = Yii::$app->db->createCommand($sql, [
  425. ':mainId' => $mainId,
  426. ':status' => HdRefundClass::STATUS_COMPLETE,
  427. ':sameDay' => self::SAME_DAY_NO,
  428. ':start' => $startTime,
  429. ':end' => $endTime,
  430. ])->queryOne();
  431. return bcadd($row['total'] ?? '0', '0', 2);
  432. }
  433. /**
  434. * 渠道收入对冲:拉取 HD 隔天成功售后及原单支付字段(扣零售 ls 渠道)
  435. * @return array
  436. */
  437. public static function listForChannelIncomeDeduct($mainId, $startTime, $endTime)
  438. {
  439. $sql = "SELECT r.refundPrice AS amount, r.hasReturn, r.returnBalance, r.couldReturn,
  440. r.onlinePay AS refundOnlinePay, r.payWay AS refundPayWay,
  441. IFNULL(o.id, r.orderId) AS orderId, o.debtPrice, o.remainDebtPrice, o.onlinePay,
  442. o.payWay AS orderPayWay, o.payWay
  443. FROM xhHdRefund r
  444. LEFT JOIN xhOrder o ON o.orderSn = r.orderSn AND IFNULL(r.orderSn,'') <> ''
  445. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  446. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
  447. $rows = Yii::$app->db->createCommand($sql, [
  448. ':mainId' => $mainId,
  449. ':status' => HdRefundClass::STATUS_COMPLETE,
  450. ':sameDay' => self::SAME_DAY_NO,
  451. ':start' => $startTime,
  452. ':end' => $endTime,
  453. ])->queryAll();
  454. return $rows ?: [];
  455. }
  456. /**
  457. * 客户业绩列表用的稳定 key,与批发 xhGhsCustom.id 隔离,避免撞号串户
  458. */
  459. public static function customStatKey($customId)
  460. {
  461. return 'hd_' . intval($customId);
  462. }
  463. /**
  464. * 客户业绩展示名:统一加「零售-」前缀,便于与批发客户区分
  465. */
  466. public static function displayCustomName($name, $customId = 0)
  467. {
  468. $name = trim((string)$name);
  469. if ($name === '') {
  470. $name = intval($customId) > 0 ? ('客户' . intval($customId)) : '零售客户';
  471. }
  472. // 已带前缀则不再重复
  473. if (mb_strpos($name, '零售-') === 0) {
  474. return $name;
  475. }
  476. return '零售-' . $name;
  477. }
  478. /**
  479. * 补全售后单上缺失的客户名(优先退款单 customName,再查 xhCustom)
  480. * @param array $rows [['customId'=>, 'customName'=>], ...]
  481. * @return array customId => ['name'=>, 'py'=>]
  482. */
  483. protected static function resolveCustomNameMap($rows)
  484. {
  485. $map = [];
  486. $needIds = [];
  487. foreach ($rows as $row) {
  488. $cid = intval($row['customId'] ?? 0);
  489. if ($cid <= 0) {
  490. continue;
  491. }
  492. $name = trim((string)($row['customName'] ?? ''));
  493. if ($name !== '') {
  494. $map[$cid] = ['name' => $name, 'py' => ''];
  495. } else {
  496. $needIds[$cid] = 1;
  497. }
  498. }
  499. if (!empty($needIds)) {
  500. $customs = CustomClass::getAllByCondition(
  501. ['id' => ['in', array_keys($needIds)]],
  502. null,
  503. 'id,name,py',
  504. 'id'
  505. );
  506. foreach ($needIds as $cid => $_) {
  507. if (!empty($customs[$cid])) {
  508. $map[$cid] = [
  509. 'name' => $customs[$cid]['name'] ?? '',
  510. 'py' => $customs[$cid]['py'] ?? '',
  511. ];
  512. }
  513. }
  514. }
  515. return $map;
  516. }
  517. /**
  518. * 按客户汇总隔天退款金额(客户业绩等)
  519. * @return array customId => ['amount'=>string, 'customName'=>string, 'py'=>string]
  520. */
  521. public static function sumAmountGroupByCustom($mainId, $startTime, $endTime)
  522. {
  523. $sql = "SELECT r.customId, COALESCE(SUM(r.refundPrice),0) AS total,
  524. MAX(NULLIF(r.customName,'')) AS customName
  525. FROM xhHdRefund r
  526. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  527. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  528. GROUP BY r.customId";
  529. $rows = Yii::$app->db->createCommand($sql, [
  530. ':mainId' => $mainId,
  531. ':status' => HdRefundClass::STATUS_COMPLETE,
  532. ':sameDay' => self::SAME_DAY_NO,
  533. ':start' => $startTime,
  534. ':end' => $endTime,
  535. ])->queryAll();
  536. $nameMap = self::resolveCustomNameMap($rows ?: []);
  537. $map = [];
  538. foreach ($rows ?: [] as $row) {
  539. $cid = intval($row['customId'] ?? 0);
  540. $info = $nameMap[$cid] ?? ['name' => '', 'py' => ''];
  541. $map[$cid] = [
  542. 'amount' => bcadd($row['total'] ?? '0', '0', 2),
  543. 'customName' => $info['name'] ?? '',
  544. 'py' => $info['py'] ?? '',
  545. ];
  546. }
  547. return $map;
  548. }
  549. /**
  550. * 按客户汇总隔天退货数量(退货退款)
  551. * @return array customId => ['num'=>string, 'customName'=>string, 'py'=>string]
  552. */
  553. public static function sumItemQtyByCustom($mainId, $startTime, $endTime)
  554. {
  555. $sql = "SELECT r.customId, COALESCE(SUM(i.num),0) AS num,
  556. MAX(NULLIF(r.customName,'')) AS customName
  557. FROM xhHdRefundItem i
  558. INNER JOIN xhHdRefund r ON r.refundSn = i.refundSn
  559. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  560. AND r.refundType=:rtype
  561. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  562. GROUP BY r.customId";
  563. $rows = Yii::$app->db->createCommand($sql, [
  564. ':mainId' => $mainId,
  565. ':status' => HdRefundClass::STATUS_COMPLETE,
  566. ':sameDay' => self::SAME_DAY_NO,
  567. ':rtype' => HdRefundClass::REFUND_TYPE_MONEY_GOOD,
  568. ':start' => $startTime,
  569. ':end' => $endTime,
  570. ])->queryAll();
  571. $nameMap = self::resolveCustomNameMap($rows ?: []);
  572. $map = [];
  573. foreach ($rows ?: [] as $row) {
  574. $cid = intval($row['customId'] ?? 0);
  575. $info = $nameMap[$cid] ?? ['name' => '', 'py' => ''];
  576. $map[$cid] = [
  577. 'num' => bcadd($row['num'] ?? '0', '0', 2),
  578. 'customName' => $info['name'] ?? '',
  579. 'py' => $info['py'] ?? '',
  580. ];
  581. }
  582. return $map;
  583. }
  584. /**
  585. * 按花材汇总隔天退货数量与金额(库存/分类收入统计)
  586. * @return array itemId => [num, amount]
  587. */
  588. public static function sumItemByProduct($mainId, $startTime, $endTime)
  589. {
  590. $sql = "SELECT i.itemId AS productId, COALESCE(SUM(i.num),0) AS num, COALESCE(SUM(i.price),0) AS amount
  591. FROM xhHdRefundItem i
  592. INNER JOIN xhHdRefund r ON r.refundSn = i.refundSn
  593. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  594. AND r.refundType=:rtype
  595. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  596. GROUP BY i.itemId";
  597. $rows = Yii::$app->db->createCommand($sql, [
  598. ':mainId' => $mainId,
  599. ':status' => HdRefundClass::STATUS_COMPLETE,
  600. ':sameDay' => self::SAME_DAY_NO,
  601. ':rtype' => HdRefundClass::REFUND_TYPE_MONEY_GOOD,
  602. ':start' => $startTime,
  603. ':end' => $endTime,
  604. ])->queryAll();
  605. $map = [];
  606. foreach ($rows as $row) {
  607. $map[$row['productId']] = [
  608. 'num' => bcadd($row['num'] ?? '0', '0', 2),
  609. 'amount' => bcadd($row['amount'] ?? '0', '0', 2),
  610. ];
  611. }
  612. return $map;
  613. }
  614. /**
  615. * 隔天「仅退款」按通过日汇总(销售仅退款)
  616. */
  617. public static function sumMoneyOnlyAmountByMainAndTime($mainId, $startTime, $endTime)
  618. {
  619. $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhHdRefund
  620. WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
  621. AND refundType=:rtype
  622. AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end";
  623. $row = Yii::$app->db->createCommand($sql, [
  624. ':mainId' => $mainId,
  625. ':status' => HdRefundClass::STATUS_COMPLETE,
  626. ':sameDay' => self::SAME_DAY_NO,
  627. ':rtype' => HdRefundClass::REFUND_TYPE_MONEY,
  628. ':start' => $startTime,
  629. ':end' => $endTime,
  630. ])->queryOne();
  631. return bcadd($row['total'] ?? '0', '0', 2);
  632. }
  633. /**
  634. * 隔天「仅退款」按通过日分摊到原单花材(花材销量页:只扣金额不扣数量)
  635. * @return array productId => [num=>0, amount=>...]
  636. */
  637. public static function sumMoneyOnlyAmountByProduct($mainId, $startTime, $endTime)
  638. {
  639. $sql = "SELECT r.id, r.refundPrice, r.orderSn
  640. FROM xhHdRefund r
  641. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  642. AND r.refundType=:rtype
  643. AND IFNULL(NULLIF(r.orderSn,''), '') <> ''
  644. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
  645. $rows = Yii::$app->db->createCommand($sql, [
  646. ':mainId' => $mainId,
  647. ':status' => HdRefundClass::STATUS_COMPLETE,
  648. ':sameDay' => self::SAME_DAY_NO,
  649. ':rtype' => HdRefundClass::REFUND_TYPE_MONEY,
  650. ':start' => $startTime,
  651. ':end' => $endTime,
  652. ])->queryAll();
  653. if (empty($rows)) {
  654. return [];
  655. }
  656. $map = [];
  657. foreach ($rows as $row) {
  658. $refundPrice = bcadd((string)($row['refundPrice'] ?? '0'), '0', 2);
  659. if (bccomp($refundPrice, '0', 2) <= 0) {
  660. continue;
  661. }
  662. $orderSn = $row['orderSn'] ?? '';
  663. $items = \bizHd\order\classes\OrderItemClass::getAllByCondition(
  664. ['orderSn' => $orderSn],
  665. null,
  666. 'itemId,num,unitPrice',
  667. null,
  668. true
  669. );
  670. if (empty($items)) {
  671. continue;
  672. }
  673. $weights = [];
  674. $weightSum = '0.00';
  675. foreach ($items as $item) {
  676. $pid = intval($item->itemId ?? 0);
  677. if ($pid <= 0) {
  678. continue;
  679. }
  680. $line = bcmul((string)($item->num ?? '0'), (string)($item->unitPrice ?? '0'), 2);
  681. if (bccomp($line, '0', 2) <= 0) {
  682. continue;
  683. }
  684. $weights[$pid] = bcadd($weights[$pid] ?? '0', $line, 2);
  685. $weightSum = bcadd($weightSum, $line, 2);
  686. }
  687. if (bccomp($weightSum, '0', 2) <= 0) {
  688. continue;
  689. }
  690. $allocated = '0.00';
  691. $pids = array_keys($weights);
  692. $last = count($pids) - 1;
  693. foreach ($pids as $idx => $pid) {
  694. if ($idx === $last) {
  695. $part = bcsub($refundPrice, $allocated, 2);
  696. } else {
  697. $part = bcdiv(bcmul($refundPrice, $weights[$pid], 4), $weightSum, 2);
  698. $allocated = bcadd($allocated, $part, 2);
  699. }
  700. if (!isset($map[$pid])) {
  701. $map[$pid] = ['num' => '0.00', 'amount' => '0.00'];
  702. }
  703. $map[$pid]['amount'] = bcadd($map[$pid]['amount'], $part, 2);
  704. }
  705. }
  706. return $map;
  707. }
  708. /**
  709. * 支付日净销量:当天已退 = refundNum - nextRefundNum(与 ghs 同口径,可复用)
  710. */
  711. public static function payDayRemainNum($xhNum, $refundNum, $nextRefundNum = 0)
  712. {
  713. $sameDayRefund = bcsub((string)$refundNum, (string)($nextRefundNum ?? 0), 2);
  714. if (bccomp($sameDayRefund, '0', 2) < 0) {
  715. $sameDayRefund = '0';
  716. }
  717. return bcsub((string)$xhNum, $sameDayRefund, 2);
  718. }
  719. }