NextDayRefundClass.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. <?php
  2. /**
  3. * 隔天售后业务类(biz-ghs/order/classes)
  4. * 用途:资格校验、支付快照与资金三态、隔天金额/返余额、统计净销量辅助。
  5. * 调用方:RefundOrderClass、CgRefundClass、统计类、售后 Controller/Service。
  6. */
  7. namespace bizGhs\order\classes;
  8. use biz\shop\classes\ShopClass;
  9. use bizGhs\custom\classes\CustomClass;
  10. use bizGhs\custom\services\GhsRechargeSettleService;
  11. use bizGhs\product\classes\ProductClass;
  12. use bizGhs\shop\classes\MainClass;
  13. use bizGhs\stock\classes\StockRecordClass;
  14. use bizHd\cg\classes\CgRefundClass;
  15. use bizHd\purchase\classes\PurchaseClass;
  16. use common\components\dict;
  17. use common\components\util;
  18. use Yii;
  19. class NextDayRefundClass
  20. {
  21. const SAME_DAY_YES = 1;
  22. const SAME_DAY_NO = 0;
  23. /** 三态开关:否 */
  24. const FLAG_NO = 0;
  25. /** 三态开关:是 */
  26. const FLAG_YES = 1;
  27. /**
  28. * 是否有有效支付时间(售后唯一时间依据;无支付时间不能售后)
  29. */
  30. public static function hasValidPayTime($order)
  31. {
  32. if (empty($order)) {
  33. return false;
  34. }
  35. $payTime = $order->payTime ?? '';
  36. return !empty($payTime) && $payTime !== '0000-00-00 00:00:00';
  37. }
  38. /**
  39. * 售后入口校验:无支付时间直接拦截
  40. */
  41. public static function assertCanRefund($order)
  42. {
  43. if (empty($order)) {
  44. util::fail('没有原订单');
  45. }
  46. if (!self::hasValidPayTime($order)) {
  47. util::fail('订单无支付时间,不能售后');
  48. }
  49. }
  50. /**
  51. * 原单是否可走隔天售后(即原「冲销」路径):
  52. * - 非当天支付(仅看 payTime);或
  53. * - 挂账已结清;或
  54. * - 已结账单(当天已结账也按冲销处理,不走普通售后)
  55. * @return array{ok:bool,reason:string,sameDay:int}
  56. */
  57. public static function checkEligible($order)
  58. {
  59. if (empty($order)) {
  60. return ['ok' => false, 'reason' => '没有原订单', 'sameDay' => self::SAME_DAY_YES, 'hasPayTime' => 0];
  61. }
  62. // 只用支付时间;无支付时间本单不能售后(由 assertCanRefund 拦截)
  63. if (!self::hasValidPayTime($order)) {
  64. return [
  65. 'ok' => false,
  66. 'reason' => '订单无支付时间,不能售后',
  67. 'sameDay' => self::SAME_DAY_YES,
  68. 'hasPayTime' => 0,
  69. ];
  70. }
  71. $payTime = $order->payTime;
  72. $isToday = date('Y-m-d', strtotime($payTime)) === date('Y-m-d');
  73. $debtPrice = bcadd((string)($order->debtPrice ?? '0'), '0', 2);
  74. $remainDebt = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
  75. $debtCleared = bccomp($debtPrice, '0', 2) > 0 && bccomp($remainDebt, '0', 2) === 0;
  76. // 已结账:当天也强制走冲销(sameDay=0),与隔天同一套资金/金额逻辑
  77. $orderCleared = !empty($order->clearId);
  78. if ($isToday && !$debtCleared && !$orderCleared) {
  79. return [
  80. 'ok' => false,
  81. 'reason' => '当天未结账订单请走普通售后;已结账/挂账结清/隔天可售后',
  82. 'sameDay' => self::SAME_DAY_YES,
  83. 'hasPayTime' => 1,
  84. ];
  85. }
  86. return [
  87. 'ok' => true,
  88. 'reason' => '',
  89. 'sameDay' => self::SAME_DAY_NO,
  90. 'orderCleared' => $orderCleared ? 1 : 0,
  91. 'hasPayTime' => 1,
  92. ];
  93. }
  94. /**
  95. * 创建/审核时是否必须按隔天售后(sameDay=0):
  96. * 与 checkEligible 对齐——非当天 / 已结账单 / 挂账已结清,均不可再走当天售后。
  97. * 无支付时间返回 false(须先走 assertCanRefund)。
  98. */
  99. public static function mustUseNextDay($order)
  100. {
  101. if (!self::hasValidPayTime($order)) {
  102. return false;
  103. }
  104. $check = self::checkEligible($order);
  105. return !empty($check['ok']);
  106. }
  107. /**
  108. * 是否微信/支付宝线上支付(看订单或售后快照)
  109. */
  110. public static function isOnlinePayOrder($orderOrRefund)
  111. {
  112. if (empty($orderOrRefund)) {
  113. return false;
  114. }
  115. $payWay = intval($orderOrRefund->payWay ?? 0);
  116. $onlinePay = intval($orderOrRefund->onlinePay ?? dict::getDict('onlinePay', 'not'));
  117. $wxPay = dict::getDict('payWay', 'wxPay');
  118. $aliPay = dict::getDict('payWay', 'alipay');
  119. return $onlinePay == dict::getDict('onlinePay', 'yes')
  120. && in_array($payWay, [$wxPay, $aliPay], true);
  121. }
  122. /**
  123. * 余额付或挂账:原路就是动余额/欠款,禁止再「返充」
  124. */
  125. public static function isBalanceOrDebtPayWay($payWay)
  126. {
  127. $payWay = intval($payWay);
  128. return $payWay === intval(dict::getDict('payWay', 'balancePay'))
  129. || $payWay === intval(dict::getDict('payWay', 'debtPay'));
  130. }
  131. /**
  132. * 是否需人工确认原路(现金/银行卡/线下微信支付宝等)
  133. * 线上通道成功、余额/挂账账务完成可自动 hasReturn;线下钱是否退还要人确认。
  134. */
  135. public static function needsManualReturnConfirm($orderOrRefund)
  136. {
  137. if (empty($orderOrRefund)) {
  138. return false;
  139. }
  140. if (self::isOnlinePayOrder($orderOrRefund)) {
  141. return false;
  142. }
  143. $payWay = intval($orderOrRefund->payWay ?? 0);
  144. if (self::isBalanceOrDebtPayWay($payWay)) {
  145. return false;
  146. }
  147. return true;
  148. }
  149. /**
  150. * 是否待选手选资金落地(成功页二选一)
  151. */
  152. public static function needFundAction($refund)
  153. {
  154. if (empty($refund)) {
  155. return 0;
  156. }
  157. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  158. return 0;
  159. }
  160. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  161. return 0;
  162. }
  163. if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) {
  164. return 0;
  165. }
  166. return 1;
  167. }
  168. /**
  169. * 生成售后资金快照(落 xhRefund/xhCgRefund)
  170. * - 有原单:onlinePay/payWay 与订单一致;余额/挂账 couldReturn=0,其它=1
  171. * - 无原单:线下渠道由选手选 payWay;couldReturn=1(成功页可改走余额)
  172. *
  173. * @param object|null $order 原销售单;无原单传 null
  174. * @param bool $hasRelateOrder 是否有关联原单
  175. * @param array $options payWay(无原单线下渠道)
  176. * @return array{onlinePay:int,payWay:int,hasReturn:int,couldReturn:int,returnBalance:int}
  177. */
  178. public static function buildRefundPaySnapshot($order = null, $hasRelateOrder = true, $options = [])
  179. {
  180. $onlineNot = intval(dict::getDict('onlinePay', 'not'));
  181. // 兼容旧调用名:内部仍可被 buildRefundFundSnapshot 转发
  182. if (!$hasRelateOrder || empty($order)) {
  183. $payWay = self::normalizeOfflinePayWay($options['payWay'] ?? null, null);
  184. return [
  185. 'onlinePay' => $onlineNot,
  186. 'payWay' => $payWay,
  187. 'hasReturn' => self::FLAG_NO,
  188. 'couldReturn' => self::FLAG_YES,
  189. 'returnBalance' => self::FLAG_NO,
  190. ];
  191. }
  192. $payWay = intval($order->payWay ?? 0);
  193. $onlinePay = intval($order->onlinePay ?? $onlineNot);
  194. $couldReturn = self::isBalanceOrDebtPayWay($payWay) ? self::FLAG_NO : self::FLAG_YES;
  195. return [
  196. 'onlinePay' => $onlinePay,
  197. 'payWay' => $payWay,
  198. 'hasReturn' => self::FLAG_NO,
  199. 'couldReturn' => $couldReturn,
  200. 'returnBalance' => self::FLAG_NO,
  201. ];
  202. }
  203. /**
  204. * @deprecated 已废弃 fundType;保留转发以免旧调用报错
  205. */
  206. public static function buildRefundFundSnapshot($fundTypeInput, $order = null, $hasRelateOrder = true, $options = [])
  207. {
  208. return self::buildRefundPaySnapshot($order, $hasRelateOrder, $options);
  209. }
  210. /**
  211. * 线下转账渠道:允许 0微信/1支付宝/4现金/5银行卡;非法则回落原单或现金
  212. */
  213. protected static function normalizeOfflinePayWay($payWay, $order = null)
  214. {
  215. $allowed = [
  216. intval(dict::getDict('payWay', 'wxPay')),
  217. intval(dict::getDict('payWay', 'alipay')),
  218. intval(dict::getDict('payWay', 'cash')),
  219. intval(dict::getDict('payWay', 'bankCard')),
  220. ];
  221. if ($payWay !== null && $payWay !== '' && in_array(intval($payWay), $allowed, true)) {
  222. return intval($payWay);
  223. }
  224. if (!empty($order)) {
  225. $orderPayWay = intval($order->payWay ?? -1);
  226. if (in_array($orderPayWay, $allowed, true)) {
  227. return $orderPayWay;
  228. }
  229. }
  230. return intval(dict::getDict('payWay', 'cash'));
  231. }
  232. /**
  233. * 售后单与采购镜像同步资金三态
  234. */
  235. public static function syncRefundFundFlags($refund, $fields = [])
  236. {
  237. if (empty($refund) || empty($fields)) {
  238. return;
  239. }
  240. foreach ($fields as $k => $v) {
  241. $refund->$k = $v;
  242. }
  243. $refund->save(false, array_keys($fields));
  244. $cgRefundId = intval($refund->cgRefundId ?? 0);
  245. if ($cgRefundId <= 0) {
  246. return;
  247. }
  248. $cgRefund = CgRefundClass::getById($cgRefundId, true);
  249. if (empty($cgRefund)) {
  250. return;
  251. }
  252. foreach ($fields as $k => $v) {
  253. $cgRefund->$k = $v;
  254. }
  255. $cgRefund->save(false, array_keys($fields));
  256. }
  257. /**
  258. * 标记已原路退回(现金/银行卡等线下人工确认)
  259. */
  260. public static function markHasReturn($refund)
  261. {
  262. if (empty($refund)) {
  263. util::fail('退款单不存在');
  264. }
  265. if (intval($refund->status) !== RefundOrderClass::STATUS_COMPLETE) {
  266. util::fail('售后未通过,不能确认原路退回');
  267. }
  268. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  269. util::fail('已返充余额,不能再标记原路退回');
  270. }
  271. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  272. return $refund;
  273. }
  274. self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]);
  275. return RefundOrderClass::getById($refund->id, true);
  276. }
  277. /**
  278. * 不想线下原路时:返充到客户余额(须 couldReturn=1 且尚未原路)
  279. * @return array{refund:object,customBalance:string}
  280. */
  281. public static function applyReturnBalance($refund, $order = null)
  282. {
  283. if (empty($refund)) {
  284. util::fail('退款单不存在');
  285. }
  286. if (intval($refund->status) !== RefundOrderClass::STATUS_COMPLETE) {
  287. util::fail('售后未通过,不能返充余额');
  288. }
  289. if (intval($refund->hasReturn ?? 0) === self::FLAG_YES) {
  290. util::fail('已原路退回,不能再返充余额');
  291. }
  292. if (intval($refund->returnBalance ?? 0) === self::FLAG_YES) {
  293. util::fail('已返充到余额');
  294. }
  295. if (intval($refund->couldReturn ?? 0) !== self::FLAG_YES) {
  296. util::fail('该单不允许返充到余额');
  297. }
  298. $customId = intval($refund->customId ?? 0);
  299. $custom = CustomClass::getLockById($customId);
  300. if (empty($custom)) {
  301. util::fail('没有找到客户');
  302. }
  303. $pair = GhsRechargeSettleService::lockAccountPair($custom);
  304. CustomClass::nextDayRefundReturnBalance($pair['custom'], $pair['ghs'], $refund, $order);
  305. self::syncRefundFundFlags($refund, ['returnBalance' => self::FLAG_YES]);
  306. $custom = CustomClass::getById($customId, true);
  307. return [
  308. 'refund' => RefundOrderClass::getById($refund->id, true),
  309. 'customBalance' => bcadd((string)($custom->balance ?? '0'), '0', 2),
  310. ];
  311. }
  312. /**
  313. * GHS 侧隔天通过:累计 nextDayTkPrice(不改 actPrice);扣 buyAmount;
  314. * 余额/挂账原路加回客户余额;线上付扣门店可提现余额(与当天售后一致,必须在 ghs 端做)。
  315. * @return string 当前客户余额
  316. */
  317. public static function applyGhsAmountAndFund($refund, $order)
  318. {
  319. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  320. $order->nextDayTkPrice = bcadd((string)($order->nextDayTkPrice ?? '0'), $refundPrice, 2);
  321. $order->refund = OrderClass::REFUND_YES;
  322. $order->save(false, ['nextDayTkPrice', 'refund']);
  323. $customId = intval($order->customId ?? 0);
  324. $custom = CustomClass::getLockById($customId);
  325. if (empty($custom)) {
  326. util::fail('没有找到客户');
  327. }
  328. // 累计消费与是否当天无关:退了就要从 buyAmount 扣掉
  329. $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2);
  330. if (bccomp($buyAmount, '0', 2) < 0) {
  331. $buyAmount = '0.00';
  332. }
  333. $custom->buyAmount = $buyAmount;
  334. $custom->save(false, ['buyAmount']);
  335. $payWay = intval($refund->payWay ?? ($order->payWay ?? 0));
  336. $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
  337. // 余额/挂账:原路=加回余额,完成后 hasReturn=1(couldReturn 已为 0)
  338. if (self::isBalanceOrDebtPayWay($payWay)) {
  339. $pair = GhsRechargeSettleService::lockAccountPair($custom);
  340. CustomClass::nextDayRefundReturnBalance($pair['custom'], $pair['ghs'], $refund, $order);
  341. self::syncRefundFundFlags($refund, ['hasReturn' => self::FLAG_YES]);
  342. $custom = CustomClass::getById($customId, true);
  343. $balance = bcadd((string)($custom->balance ?? '0'), '0', 2);
  344. } elseif (self::isOnlinePayOrder($refund) || self::isOnlinePayOrder($order)) {
  345. // 线上付收款时加过门店可提现余额,隔天售后也要在 ghs 端扣回(拉卡拉原路仍在采购侧发起)
  346. $shopId = intval($order->shopId ?? ($refund->shopId ?? 0));
  347. $mainId = intval($order->mainId ?? ($refund->mainId ?? 0));
  348. $shop = ShopClass::getLockById($shopId);
  349. if (empty($shop)) {
  350. util::fail('没有找到门店');
  351. }
  352. $main = MainClass::getLockById($mainId);
  353. if (empty($main)) {
  354. util::fail('没有main信息');
  355. }
  356. ShopClass::saleRefundReduceBalance($main, $shop, $refund, $refundPrice);
  357. }
  358. // 现金/银行卡等线下原路等人工 markHasReturn
  359. return $balance;
  360. }
  361. /**
  362. * HD 采购侧隔天通过:累计 nextDayTkPrice;线上原路走拉卡拉成功后 hasReturn=1。
  363. */
  364. public static function applyCgAmountAndFund($cgRefund, $cg)
  365. {
  366. $refundPrice = bcadd((string)($cgRefund->refundPrice ?? '0'), '0', 2);
  367. $cg->nextDayTkPrice = bcadd((string)($cg->nextDayTkPrice ?? '0'), $refundPrice, 2);
  368. $cg->refund = PurchaseClass::REFUND_YES;
  369. $cg->save(false, ['nextDayTkPrice', 'refund']);
  370. if (self::isOnlinePayOrder($cgRefund) || self::isOnlinePayOrder($cg)) {
  371. CgRefundClass::nextDayOriginalOnlineRefund($cgRefund, $cg);
  372. $cgRefund->hasReturn = self::FLAG_YES;
  373. $cgRefund->save(false, ['hasReturn']);
  374. $saleRefundId = intval($cgRefund->saleRefundId ?? 0);
  375. if ($saleRefundId > 0) {
  376. $saleRefund = RefundOrderClass::getById($saleRefundId, true);
  377. if (!empty($saleRefund)) {
  378. self::syncRefundFundFlags($saleRefund, ['hasReturn' => self::FLAG_YES]);
  379. }
  380. }
  381. }
  382. $ghsId = intval($cg->ghsId ?? 0);
  383. if ($ghsId > 0) {
  384. $ghs = \bizHd\ghs\classes\GhsClass::getLockById($ghsId);
  385. if (!empty($ghs) && isset($ghs->expendAmount)) {
  386. $expend = bcsub((string)($ghs->expendAmount ?? '0'), $refundPrice, 2);
  387. if (bccomp($expend, '0', 2) < 0) {
  388. $expend = '0.00';
  389. }
  390. $ghs->expendAmount = $expend;
  391. $ghs->save(false, ['expendAmount']);
  392. }
  393. }
  394. }
  395. /**
  396. * 无原单退款通过:加库存(退货退款)、累计门店支出、扣客户消费;资金留给成功页确认原路/返充。
  397. * 不写订单 nextDayTkPrice(无关联销售单)。
  398. */
  399. public static function passFreeRefund($refund)
  400. {
  401. if (empty($refund)) {
  402. util::fail('退款单不存在');
  403. }
  404. if (intval($refund->status) === RefundOrderClass::STATUS_COMPLETE) {
  405. util::fail('已通过');
  406. }
  407. if (intval($refund->status) === RefundOrderClass::STATUS_REJECT) {
  408. util::fail('已驳回');
  409. }
  410. if (intval($refund->status) === RefundOrderClass::STATUS_CANCEL) {
  411. util::fail('已取消');
  412. }
  413. $refund->status = RefundOrderClass::STATUS_COMPLETE;
  414. $refund->passTime = date('Y-m-d H:i:s');
  415. $refund->save();
  416. $refundSn = $refund->orderSn ?? '';
  417. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  418. $refundType = intval($refund->refundType ?? RefundOrderClass::REFUND_TYPE_MONEY_GOOD);
  419. $shopId = intval($refund->shopId ?? 0);
  420. $mainId = intval($refund->mainId ?? 0);
  421. $sjId = intval($refund->sjId ?? 0);
  422. $customName = $refund->customName ?? '';
  423. // 有明细则置通过;仅「退货退款」回库(库存不退只记账不改库存)
  424. $refundItemList = RefundOrderItemClass::getAllByCondition(['orderSn' => $refundSn], null, '*', null, true);
  425. if ($refundType === RefundOrderClass::REFUND_TYPE_MONEY_GOOD && empty($refundItemList)) {
  426. util::fail('没有找到退款花材');
  427. }
  428. if (!empty($refundItemList)) {
  429. foreach ($refundItemList as $val) {
  430. $val->status = 1;
  431. $val->save();
  432. if ($refundType !== RefundOrderClass::REFUND_TYPE_MONEY_GOOD) {
  433. continue;
  434. }
  435. $productId = intval($val->productId ?? 0);
  436. $ptItemId = intval($val->itemId ?? 0);
  437. $num = bcadd((string)($val->xhNum ?? '0'), '0', 2);
  438. if (bccomp($num, '0', 2) <= 0 || $productId <= 0) {
  439. continue;
  440. }
  441. if (intval($val->xhUnitType ?? 0) === 1) {
  442. $smallNum = $num;
  443. $bigNum = 0;
  444. } else {
  445. $smallNum = 0;
  446. $bigNum = $num;
  447. }
  448. $stockInfo = ProductClass::addStock($productId, $bigNum, $smallNum);
  449. ProductClass::tryRecoverLimitBuyWhenStockRollbackFromZero($productId, $stockInfo['oldStock'] ?? 0);
  450. $oldStock = $stockInfo['oldStock'] ?? 0;
  451. $newStock = $stockInfo['newStock'] ?? 0;
  452. $recordData = [
  453. 'relateName' => $customName,
  454. 'itemNum' => $val->itemNum ?? 0,
  455. 'sjId' => $sjId,
  456. 'shopId' => $shopId,
  457. 'mainId' => $mainId,
  458. 'orderSn' => $refundSn,
  459. 'itemId' => $ptItemId,
  460. 'oldStock' => $oldStock,
  461. 'productId' => $productId,
  462. 'newStock' => $newStock,
  463. 'io' => 1,
  464. ];
  465. StockRecordClass::ghsRefundAddRecord($recordData);
  466. $val->itemStock = $oldStock;
  467. $val->newStock = $newStock;
  468. $val->save();
  469. }
  470. }
  471. $main = MainClass::getLockById($mainId);
  472. if (empty($main)) {
  473. util::fail('没有main信息');
  474. }
  475. $main->totalExpend = bcadd((string)($main->totalExpend ?? '0'), $refundPrice, 2);
  476. $main->totalRefund = bcadd((string)($main->totalRefund ?? '0'), $refundPrice, 2);
  477. $main->save();
  478. return self::applyFreeFund($refund);
  479. }
  480. /**
  481. * 无原单退款资金:仅扣减客户累计消费;原路/返充在成功页由 markHasReturn / applyReturnBalance 落地。
  482. * @return string 当前客户余额
  483. */
  484. public static function applyFreeFund($refund)
  485. {
  486. $refundPrice = bcadd((string)($refund->refundPrice ?? '0'), '0', 2);
  487. $customId = intval($refund->customId ?? 0);
  488. $custom = CustomClass::getLockById($customId);
  489. if (empty($custom)) {
  490. util::fail('没有找到客户');
  491. }
  492. $buyAmount = bcsub((string)($custom->buyAmount ?? '0'), $refundPrice, 2);
  493. if (bccomp($buyAmount, '0', 2) < 0) {
  494. $buyAmount = '0.00';
  495. }
  496. $custom->buyAmount = $buyAmount;
  497. $custom->save(false, ['buyAmount']);
  498. return bcadd((string)($custom->balance ?? '0'), '0', 2);
  499. }
  500. /**
  501. * 按时段汇总成功隔天售后金额(收入/销量统计扣减用)
  502. */
  503. public static function sumAmountByMainAndTime($mainId, $startTime, $endTime)
  504. {
  505. $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhRefund
  506. WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
  507. AND passTime BETWEEN :start AND :end";
  508. $row = Yii::$app->db->createCommand($sql, [
  509. ':mainId' => $mainId,
  510. ':status' => RefundOrderClass::STATUS_COMPLETE,
  511. ':sameDay' => self::SAME_DAY_NO,
  512. ':start' => $startTime,
  513. ':end' => $endTime,
  514. ])->queryOne();
  515. return bcadd($row['total'] ?? '0', '0', 2);
  516. }
  517. /**
  518. * 渠道收入对冲:拉取隔天成功售后及原单支付字段
  519. */
  520. public static function listForChannelIncomeDeduct($mainId, $startTime, $endTime)
  521. {
  522. $sql = "SELECT r.refundPrice AS amount, r.hasReturn, r.returnBalance, r.couldReturn,
  523. r.onlinePay AS refundOnlinePay, r.payWay AS refundPayWay,
  524. o.id AS orderId, o.debtPrice, o.remainDebtPrice, o.onlinePay, o.payWay AS orderPayWay, o.payWay
  525. FROM xhRefund r
  526. LEFT JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn
  527. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  528. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
  529. $rows = Yii::$app->db->createCommand($sql, [
  530. ':mainId' => $mainId,
  531. ':status' => RefundOrderClass::STATUS_COMPLETE,
  532. ':sameDay' => self::SAME_DAY_NO,
  533. ':start' => $startTime,
  534. ':end' => $endTime,
  535. ])->queryAll();
  536. return $rows ?: [];
  537. }
  538. /**
  539. * 按客户汇总隔天退款金额
  540. */
  541. public static function sumAmountGroupByCustom($mainId, $startTime, $endTime)
  542. {
  543. $sql = "SELECT r.customId, COALESCE(SUM(r.refundPrice),0) AS total FROM xhRefund r
  544. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  545. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  546. GROUP BY r.customId";
  547. $rows = Yii::$app->db->createCommand($sql, [
  548. ':mainId' => $mainId,
  549. ':status' => RefundOrderClass::STATUS_COMPLETE,
  550. ':sameDay' => self::SAME_DAY_NO,
  551. ':start' => $startTime,
  552. ':end' => $endTime,
  553. ])->queryAll();
  554. $map = [];
  555. foreach ($rows as $row) {
  556. $map[$row['customId']] = bcadd($row['total'] ?? '0', '0', 2);
  557. }
  558. return $map;
  559. }
  560. /**
  561. * 按客户汇总隔天退货数量(仅退货退款;通过日无开单时客户业绩扎数可显示为负)
  562. * @return array customId => num
  563. */
  564. public static function sumItemQtyByCustom($mainId, $startTime, $endTime)
  565. {
  566. $sql = "SELECT r.customId, COALESCE(SUM(i.xhNum),0) AS num
  567. FROM xhRefundItem i
  568. INNER JOIN xhRefund r ON r.orderSn = i.orderSn
  569. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  570. AND r.refundType=:rtype
  571. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  572. GROUP BY r.customId";
  573. $rows = Yii::$app->db->createCommand($sql, [
  574. ':mainId' => $mainId,
  575. ':status' => RefundOrderClass::STATUS_COMPLETE,
  576. ':sameDay' => self::SAME_DAY_NO,
  577. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
  578. ':start' => $startTime,
  579. ':end' => $endTime,
  580. ])->queryAll();
  581. $map = [];
  582. foreach ($rows as $row) {
  583. $map[$row['customId']] = bcadd($row['num'] ?? '0', '0', 2);
  584. }
  585. return $map;
  586. }
  587. /**
  588. * 按花材汇总隔天退货数量与金额(退货退款明细)
  589. */
  590. public static function sumItemByProduct($mainId, $startTime, $endTime)
  591. {
  592. $sql = "SELECT i.productId, COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
  593. FROM xhRefundItem i
  594. INNER JOIN xhRefund r ON r.orderSn = i.orderSn
  595. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  596. AND r.refundType=:rtype
  597. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  598. GROUP BY i.productId";
  599. $rows = Yii::$app->db->createCommand($sql, [
  600. ':mainId' => $mainId,
  601. ':status' => RefundOrderClass::STATUS_COMPLETE,
  602. ':sameDay' => self::SAME_DAY_NO,
  603. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
  604. ':start' => $startTime,
  605. ':end' => $endTime,
  606. ])->queryAll();
  607. $map = [];
  608. foreach ($rows as $row) {
  609. $map[$row['productId']] = [
  610. 'num' => bcadd($row['num'] ?? '0', '0', 2),
  611. 'amount' => bcadd($row['amount'] ?? '0', '0', 2),
  612. ];
  613. }
  614. return $map;
  615. }
  616. /**
  617. * 支付日净销量:当天已退 = refundNum - nextRefundNum,净销 = xhNum - 当天已退。
  618. * 隔天退货记在 nextRefundNum,不在支付日从销量扣掉(通过日再扣)。
  619. *
  620. * @param mixed $xhNum 下单数量
  621. * @param mixed $refundNum 总已退
  622. * @param mixed $nextRefundNum 隔天已退
  623. * @return string
  624. */
  625. public static function payDayRemainNum($xhNum, $refundNum, $nextRefundNum = 0)
  626. {
  627. $sameDayRefund = bcsub((string)$refundNum, (string)($nextRefundNum ?? 0), 2);
  628. if (bccomp($sameDayRefund, '0', 2) < 0) {
  629. $sameDayRefund = '0';
  630. }
  631. return bcsub((string)$xhNum, $sameDayRefund, 2);
  632. }
  633. /**
  634. * @deprecated 已改用订单行 nextRefundNum + payDayRemainNum,保留兼容旧调用
  635. * 关联原单上「已发生」的隔天退货(不限通过日):用于销量按 payTime 统计时加回 refundNum 中的隔天部分,
  636. * 避免与按 passTime 扣减重复;口径对齐客户业绩(支付日先全额,通过日再扣)。
  637. *
  638. * @param int $mainId
  639. * @param string[] $relateOrderSns 原销售单号列表
  640. * @return array productId => [num, amount]
  641. */
  642. public static function sumItemByProductForRelateOrderSns($mainId, $relateOrderSns)
  643. {
  644. $relateOrderSns = array_values(array_unique(array_filter($relateOrderSns)));
  645. if (empty($relateOrderSns)) {
  646. return [];
  647. }
  648. $snParams = [];
  649. foreach ($relateOrderSns as $i => $sn) {
  650. $snParams[':sn' . $i] = $sn;
  651. }
  652. $in = implode(',', array_keys($snParams));
  653. $sql = "SELECT i.productId, COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
  654. FROM xhRefundItem i
  655. INNER JOIN xhRefund r ON r.orderSn = i.orderSn
  656. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  657. AND r.refundType=:rtype
  658. AND r.relateOrderSn IN ($in)
  659. GROUP BY i.productId";
  660. $params = array_merge([
  661. ':mainId' => $mainId,
  662. ':status' => RefundOrderClass::STATUS_COMPLETE,
  663. ':sameDay' => self::SAME_DAY_NO,
  664. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
  665. ], $snParams);
  666. $rows = Yii::$app->db->createCommand($sql, $params)->queryAll();
  667. $map = [];
  668. foreach ($rows as $row) {
  669. $map[$row['productId']] = [
  670. 'num' => bcadd($row['num'] ?? '0', '0', 2),
  671. 'amount' => bcadd($row['amount'] ?? '0', '0', 2),
  672. ];
  673. }
  674. return $map;
  675. }
  676. /**
  677. * 隔天「仅退款」按通过日汇总金额(无花材明细,进收支分类「销售仅退款」等)
  678. */
  679. public static function sumMoneyOnlyAmountByMainAndTime($mainId, $startTime, $endTime)
  680. {
  681. $sql = "SELECT COALESCE(SUM(refundPrice),0) AS total FROM xhRefund
  682. WHERE mainId=:mainId AND status=:status AND sameDay=:sameDay
  683. AND refundType=:rtype
  684. AND IFNULL(NULLIF(passTime,'0000-00-00 00:00:00'), addTime) BETWEEN :start AND :end";
  685. $row = Yii::$app->db->createCommand($sql, [
  686. ':mainId' => $mainId,
  687. ':status' => RefundOrderClass::STATUS_COMPLETE,
  688. ':sameDay' => self::SAME_DAY_NO,
  689. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY,
  690. ':start' => $startTime,
  691. ':end' => $endTime,
  692. ])->queryOne();
  693. return bcadd($row['total'] ?? '0', '0', 2);
  694. }
  695. /**
  696. * 隔天「仅退款」按通过日分摊到原单花材(无明细时按原单行金额占比分摊 refundPrice)
  697. * 花材销量页用:通过日扣金额,数量不扣。
  698. *
  699. * @return array productId => [num=>0, amount=>...]
  700. */
  701. public static function sumMoneyOnlyAmountByProduct($mainId, $startTime, $endTime)
  702. {
  703. $sql = "SELECT r.id, r.refundPrice, r.relateOrderSn
  704. FROM xhRefund r
  705. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  706. AND r.refundType=:rtype
  707. AND IFNULL(NULLIF(r.relateOrderSn,''), '') <> ''
  708. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end";
  709. $rows = Yii::$app->db->createCommand($sql, [
  710. ':mainId' => $mainId,
  711. ':status' => RefundOrderClass::STATUS_COMPLETE,
  712. ':sameDay' => self::SAME_DAY_NO,
  713. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY,
  714. ':start' => $startTime,
  715. ':end' => $endTime,
  716. ])->queryAll();
  717. if (empty($rows)) {
  718. return [];
  719. }
  720. $map = [];
  721. foreach ($rows as $row) {
  722. $refundPrice = bcadd((string)($row['refundPrice'] ?? '0'), '0', 2);
  723. if (bccomp($refundPrice, '0', 2) <= 0) {
  724. continue;
  725. }
  726. $relateSn = $row['relateOrderSn'] ?? '';
  727. $items = \bizGhs\order\classes\OrderItemClass::getAllByCondition(
  728. ['orderSn' => $relateSn],
  729. null,
  730. 'productId,xhNum,xhUnitPrice',
  731. null,
  732. true
  733. );
  734. if (empty($items)) {
  735. continue;
  736. }
  737. $weights = [];
  738. $weightSum = '0.00';
  739. foreach ($items as $item) {
  740. $pid = intval($item->productId ?? 0);
  741. if ($pid <= 0) {
  742. continue;
  743. }
  744. $line = bcmul((string)($item->xhNum ?? '0'), (string)($item->xhUnitPrice ?? '0'), 2);
  745. if (bccomp($line, '0', 2) <= 0) {
  746. continue;
  747. }
  748. $weights[$pid] = bcadd($weights[$pid] ?? '0', $line, 2);
  749. $weightSum = bcadd($weightSum, $line, 2);
  750. }
  751. if (bccomp($weightSum, '0', 2) <= 0) {
  752. continue;
  753. }
  754. $allocated = '0.00';
  755. $pids = array_keys($weights);
  756. $last = count($pids) - 1;
  757. foreach ($pids as $idx => $pid) {
  758. if ($idx === $last) {
  759. $part = bcsub($refundPrice, $allocated, 2);
  760. } else {
  761. $part = bcdiv(bcmul($refundPrice, $weights[$pid], 4), $weightSum, 2);
  762. $allocated = bcadd($allocated, $part, 2);
  763. }
  764. if (!isset($map[$pid])) {
  765. $map[$pid] = ['num' => '0.00', 'amount' => '0.00'];
  766. }
  767. $map[$pid]['amount'] = bcadd($map[$pid]['amount'], $part, 2);
  768. }
  769. }
  770. return $map;
  771. }
  772. /**
  773. * 按配送员汇总隔天退货数量/金额(通过日;原单 sendStaffId)
  774. * @return array sendStaffId => [num, amount]
  775. */
  776. public static function sumItemBySendStaff($mainId, $startTime, $endTime)
  777. {
  778. $sql = "SELECT IFNULL(o.sendStaffId,0) AS sendStaffId,
  779. COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
  780. FROM xhRefundItem i
  781. INNER JOIN xhRefund r ON r.orderSn = i.orderSn
  782. INNER JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn
  783. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  784. AND r.refundType=:rtype
  785. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  786. GROUP BY IFNULL(o.sendStaffId,0)";
  787. $rows = Yii::$app->db->createCommand($sql, [
  788. ':mainId' => $mainId,
  789. ':status' => RefundOrderClass::STATUS_COMPLETE,
  790. ':sameDay' => self::SAME_DAY_NO,
  791. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
  792. ':start' => $startTime,
  793. ':end' => $endTime,
  794. ])->queryAll();
  795. return self::mapStaffNumAmount($rows);
  796. }
  797. /**
  798. * @deprecated 已改用订单行 nextRefundNum + payDayRemainNum,保留兼容旧调用
  799. * 关联原单上已发生的隔天退货,按配送员汇总(支付日统计加回用)
  800. */
  801. public static function sumItemBySendStaffForRelateOrderSns($mainId, $relateOrderSns)
  802. {
  803. $relateOrderSns = array_values(array_unique(array_filter($relateOrderSns)));
  804. if (empty($relateOrderSns)) {
  805. return [];
  806. }
  807. $snParams = [];
  808. foreach ($relateOrderSns as $i => $sn) {
  809. $snParams[':sn' . $i] = $sn;
  810. }
  811. $in = implode(',', array_keys($snParams));
  812. $sql = "SELECT IFNULL(o.sendStaffId,0) AS sendStaffId,
  813. COALESCE(SUM(i.xhNum),0) AS num, COALESCE(SUM(i.xhPrice),0) AS amount
  814. FROM xhRefundItem i
  815. INNER JOIN xhRefund r ON r.orderSn = i.orderSn
  816. INNER JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn
  817. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  818. AND r.refundType=:rtype
  819. AND r.relateOrderSn IN ($in)
  820. GROUP BY IFNULL(o.sendStaffId,0)";
  821. $params = array_merge([
  822. ':mainId' => $mainId,
  823. ':status' => RefundOrderClass::STATUS_COMPLETE,
  824. ':sameDay' => self::SAME_DAY_NO,
  825. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY_GOOD,
  826. ], $snParams);
  827. $rows = Yii::$app->db->createCommand($sql, $params)->queryAll();
  828. return self::mapStaffNumAmount($rows);
  829. }
  830. /**
  831. * 隔天仅退款按配送员汇总金额(通过日)
  832. */
  833. public static function sumMoneyOnlyAmountBySendStaff($mainId, $startTime, $endTime)
  834. {
  835. $sql = "SELECT IFNULL(o.sendStaffId,0) AS sendStaffId, COALESCE(SUM(r.refundPrice),0) AS amount
  836. FROM xhRefund r
  837. INNER JOIN xhGhsOrder o ON o.orderSn = r.relateOrderSn
  838. WHERE r.mainId=:mainId AND r.status=:status AND r.sameDay=:sameDay
  839. AND r.refundType=:rtype
  840. AND IFNULL(NULLIF(r.relateOrderSn,''), '') <> ''
  841. AND IFNULL(NULLIF(r.passTime,'0000-00-00 00:00:00'), r.addTime) BETWEEN :start AND :end
  842. GROUP BY IFNULL(o.sendStaffId,0)";
  843. $rows = Yii::$app->db->createCommand($sql, [
  844. ':mainId' => $mainId,
  845. ':status' => RefundOrderClass::STATUS_COMPLETE,
  846. ':sameDay' => self::SAME_DAY_NO,
  847. ':rtype' => RefundOrderClass::REFUND_TYPE_MONEY,
  848. ':start' => $startTime,
  849. ':end' => $endTime,
  850. ])->queryAll();
  851. $map = [];
  852. foreach ($rows as $row) {
  853. $sid = intval($row['sendStaffId'] ?? 0);
  854. $map[$sid] = [
  855. 'num' => '0.00',
  856. 'amount' => bcadd($row['amount'] ?? '0', '0', 2),
  857. ];
  858. }
  859. return $map;
  860. }
  861. /**
  862. * @param array $rows
  863. * @return array
  864. */
  865. protected static function mapStaffNumAmount($rows)
  866. {
  867. $map = [];
  868. foreach ($rows as $row) {
  869. $sid = intval($row['sendStaffId'] ?? 0);
  870. $map[$sid] = [
  871. 'num' => bcadd($row['num'] ?? '0', '0', 2),
  872. 'amount' => bcadd($row['amount'] ?? '0', '0', 2),
  873. ];
  874. }
  875. return $map;
  876. }
  877. }