OrderClearClass.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. namespace bizGhs\order\classes;
  3. use biz\ghs\classes\GhsClass;
  4. use biz\shop\classes\ShopClass;
  5. use bizGhs\custom\classes\CustomClass;
  6. use bizGhs\shop\classes\MainClass;
  7. use bizGhs\shop\classes\ShopMoneyChangeClass;
  8. use common\components\dict;
  9. use common\components\noticeUtil;
  10. use common\components\orderSn;
  11. use common\components\util;
  12. use bizGhs\base\classes\BaseClass;
  13. use bizGhs\clear\classes\OrderCgClearClass;
  14. use bizGhs\custom\services\GhsRechargeSettleService;
  15. use bizHd\purchase\classes\PurchaseClass;
  16. use bizHd\purchase\classes\PurchaseClearClass;
  17. class OrderClearClass extends BaseClass
  18. {
  19. public static $baseFile = '\bizGhs\order\models\OrderClear';
  20. /**
  21. * 使用账户正余额 FIFO 结挂账(GhsRechargeSettleService::consumePositiveBalanceFifo)
  22. */
  23. public static function useBalanceClear($custom, $ghs, $shop, $staff)
  24. {
  25. return GhsRechargeSettleService::consumePositiveBalanceFifo($custom, $ghs, $shop, $staff);
  26. }
  27. /** 作废该客户所有待付结账单(status=待付),新建销账前调用 */
  28. public static function expireAwaitPayClears($customId)
  29. {
  30. $list = self::getAllByCondition(['customId' => $customId, 'status' => PurchaseClearClass::STATUS_AWAIT_PAY], null, '*', null, true);
  31. if (empty($list)) {
  32. return;
  33. }
  34. foreach ($list as $item) {
  35. $item->status = PurchaseClearClass::STATUS_EXPIRE;
  36. $item->save();
  37. OrderCgClearClass::markExpireByClearId($item->id);
  38. }
  39. }
  40. /** 汇总结账单下各 OrderCgClear.amount(实销合计,支持部分销单) */
  41. public static function sumClearAmountByClearId($clearId)
  42. {
  43. $clearId = intval($clearId);
  44. if ($clearId <= 0) {
  45. return '0.00';
  46. }
  47. $list = OrderCgClearClass::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
  48. $total = '0.00';
  49. if (!empty($list)) {
  50. foreach ($list as $row) {
  51. $total = bcadd($total, $row->amount ?? 0, 2);
  52. }
  53. }
  54. return $total;
  55. }
  56. /**
  57. * 按每单指定 clearAmount 建结账单(充值 FIFO 销账、末单可部分销 remainDebtPrice)
  58. *
  59. * @param array $post 同 clear()
  60. * @param array $orderRows [['orderId'=>,'clearAmount'=>,'orderSn'=>], ...]
  61. */
  62. public static function clearWithAmountMap($post, $orderRows, $sjId, $shopId)
  63. {
  64. if (empty($orderRows)) {
  65. util::fail('请选择订单');
  66. }
  67. $ids = [];
  68. $amountMap = [];
  69. foreach ($orderRows as $row) {
  70. $orderId = intval($row['orderId'] ?? 0);
  71. if ($orderId <= 0) {
  72. continue;
  73. }
  74. $ids[] = $orderId;
  75. $amountMap[$orderId] = bcadd((string)($row['clearAmount'] ?? 0), '0', 2);
  76. }
  77. if (empty($ids)) {
  78. util::fail('请选择订单');
  79. }
  80. $idText = json_encode(array_map(function ($id) {
  81. return ['id' => $id];
  82. }, $ids));
  83. $post['modifyPrice'] = $post['modifyPrice'] ?? array_sum(array_map('floatval', $amountMap));
  84. $clear = self::clear($post, $idText, $sjId, $shopId, $amountMap);
  85. return $clear;
  86. }
  87. //订单结算 ssh 2021.3.14
  88. public static function clear($post, $idText, $sjId, $shopId, $orderAmountMap = [])
  89. {
  90. $customId = $post['customId'] ?? 0;
  91. $data = json_decode($idText, true);
  92. $ids = array_column($data, 'id');
  93. if (empty($ids)) {
  94. util::fail('请选择交易记录');
  95. }
  96. $list = OrderClass::getByIds($ids);
  97. if (empty($list)) {
  98. util::fail('请选择订单');
  99. }
  100. $modifyPrice = bcadd((string)($post['modifyPrice'] ?? '0'), '0', 2);
  101. $amount = '0.00';
  102. if (!empty($orderAmountMap)) {
  103. // 充值 FIFO 销账:prePrice=Σ每单销账额(对照 hd Settle,勿用订单全款算优惠)
  104. foreach ($orderAmountMap as $mayClear) {
  105. $amount = bcadd($amount, (string)$mayClear, 2);
  106. }
  107. }
  108. foreach ($list as $key => $val) {
  109. if ($val['sjId'] != $sjId) {
  110. util::fail('只能结算自己的订单哦');
  111. }
  112. if ($val['debt'] == 0) {
  113. util::fail('有订单已经结算过了');
  114. }
  115. if ($val['customId'] != $customId) {
  116. util::fail('每次只能结算一个客户的订单');
  117. }
  118. if (empty($orderAmountMap)) {
  119. $amount = bcadd($amount, $val['remainDebtPrice'], 2);
  120. }
  121. }
  122. $custom = CustomClass::getLockById($customId);
  123. if (empty($custom)) {
  124. util::fail('没有找到客户');
  125. }
  126. // 建结账单前:历史挂账并入净 balance(关键词 ensure_before_recharge_clear)
  127. \bizGhs\custom\classes\AccountMoneyClass::ensureCustomMoneyReady($custom, true);
  128. $ghsId = $custom->ghsId;
  129. $ghs = GhsClass::getLockById($ghsId);
  130. if (empty($ghs)) {
  131. util::fail('没有找到供货商');
  132. }
  133. \bizGhs\custom\classes\AccountMoneyClass::ensureGhsMoneyReady($ghs, true);
  134. $ghsShopId = $ghs['shopId'] ?? 0;
  135. $ghsShop = ShopClass::getById($ghsShopId, true);
  136. $ghsShopName = $ghsShop->shopName ?? '';
  137. $ghsShopName = $ghsShopName == '首店' ? '总店' : $ghsShopName;
  138. $orderSn = orderSn::getPurchaseClearSn();
  139. //默认24小时过期
  140. $deadline = date("Y-m-d H:i:s", time() + 24 * 60 * 60);
  141. //天天鲜花12小时之后
  142. if ($ghsShopId == 763 || $ghsShopId == 795 || $ghsShopId == 1405) {
  143. $deadline = date("Y-m-d H:i:s", time() + 10 * 60 * 60);
  144. }
  145. //开发环境10个小时过期
  146. if (getenv('YII_ENV') != 'production') {
  147. $deadline = date("Y-m-d H:i:s", time() + 10 * 60 * 60);
  148. }
  149. //惠雅鲜花 结账单过期时间设置为30天
  150. if ($ghsShopId == 520 || $ghsShopId == 1489 || $ghsShopId == 2164 || $ghsShopId == 2167) {
  151. $deadline = date("Y-m-d H:i:s", time() + 30 * 24 * 60 * 60);
  152. }
  153. //昱成花卉 结账单过期时间设置为30天
  154. if ($ghsShopId == 8596) {
  155. $deadline = date("Y-m-d H:i:s", time() + 30 * 24 * 60 * 60);
  156. }
  157. //花大苪 结账单过期时间设置为30天
  158. if ($ghsShopId == 8249) {
  159. $deadline = date("Y-m-d H:i:s", time() + 30 * 24 * 60 * 60);
  160. }
  161. //丰行鲜花 结账单过期时间设置为30天
  162. if ($ghsShopId == 7855) {
  163. $deadline = date("Y-m-d H:i:s", time() + 30 * 24 * 60 * 60);
  164. }
  165. $payWay = isset($post['payWay']) && is_numeric($post['payWay']) ? $post['payWay'] : dict::getDict('payWay', 'unPay');
  166. $remark = $post['remark'] ?? '';
  167. $onlinePay = isset($post['onlinePay']) && is_numeric($post['onlinePay'])
  168. ? intval($post['onlinePay'])
  169. : intval(dict::getDict('onlinePay', 'not'));
  170. $clearData = [
  171. 'prePrice' => $amount,
  172. 'actPrice' => $modifyPrice,
  173. 'realPrice' => $modifyPrice,
  174. 'ghsId' => $ghsId,
  175. 'ghsName' => $ghs['name'] ?? '',
  176. 'ghsMobile' => $ghs['mobile'] ?? '',
  177. 'ghsAvatar' => $ghs['avatar'] ?? '',
  178. 'ghsAddress' => $ghs['address'] ?? '',
  179. 'ghsShopAdminId' => $post['ghsShopAdminId'],
  180. 'ghsShopId' => $post['ghsShopId'] ?? 0,
  181. 'ghsShopAdminName' => $post['ghsShopAdminName'],
  182. 'sjId' => $sjId,
  183. 'shopId' => $shopId,
  184. 'orderSn' => $orderSn,
  185. 'status' => 1,
  186. 'clearStyle' => dict::getDict('clearStyle', 'gys2Hd'),
  187. 'customId' => $customId,
  188. 'customName' => $custom['name'] ?? '',
  189. 'customShopId' => $custom['shopId'] ?? 0,
  190. 'customAvatar' => $custom['avatar'] ?? '',
  191. 'customMobile' => $custom['mobile'] ?? '',
  192. 'customAddress' => $custom['address'] ?? '',
  193. 'deadline' => $deadline,
  194. 'payWay' => $payWay,
  195. 'onlinePay' => $onlinePay,
  196. 'num' => count($ids),
  197. 'ghsShopName' => $ghsShopName,
  198. 'hasNoticeCustom' => 0,
  199. 'remark' => $remark,
  200. // 生成并保存 salt 字段,用于结账单的验签和防篡改分享链接
  201. 'salt' => $post['salt'] ?? \common\components\stringUtil::charsShuffleLowerCase(10),
  202. ];
  203. if (!empty($orderAmountMap)) {
  204. // 自动充值销账:结账单金额=本次来款池,不产生虚假「优惠」
  205. $modifyPrice = $amount;
  206. }
  207. if (bccomp($modifyPrice, $amount, 2) < 0) {
  208. $clearData['discountAmount'] = bcsub($amount, $modifyPrice, 2);
  209. $clearData['discountType'] = dict::getDict('discountType', 'discount');
  210. }
  211. if (bccomp($modifyPrice, $amount, 2) > 0) {
  212. //noticeUtil::push("结账金额大于总欠款金额 {$modifyPrice} {$amount}", '15280215347');
  213. util::fail('结账金额大于总欠款金额');
  214. }
  215. $return = PurchaseClearClass::add($clearData, true);
  216. $pairs = OrderCgClearClass::buildPairsFromOrders($list);
  217. // 充值销账:每单销账金额可小于 remainDebtPrice(部分销)
  218. if (!empty($orderAmountMap)) {
  219. foreach ($pairs as $k => $pair) {
  220. $oid = intval($pair['orderId'] ?? 0);
  221. if ($oid > 0 && isset($orderAmountMap[$oid])) {
  222. $pairs[$k]['amount'] = $orderAmountMap[$oid];
  223. }
  224. }
  225. }
  226. OrderCgClearClass::bindRelations($return, $pairs);
  227. $complete = $post['complete'] ?? 0;
  228. if ($complete == 1) {
  229. self::confirmClear($return, $payWay);
  230. }
  231. return $return;
  232. }
  233. /**
  234. * 净余额模型销账:只减订单 remainDebtPrice,不调 clearDebtAmountReduce、不二次扣 balance。
  235. * 充值/来款链路请只调本方法(GhsRechargeSettleService)。
  236. */
  237. public static function applyNetBalanceClear($clear, $payWay = 0, $options = [])
  238. {
  239. return self::confirmClear($clear, $payWay, array_merge([
  240. 'paymentSettleOnly' => true,
  241. ], $options));
  242. }
  243. /**
  244. * 确认结账(手工结账单等仍可能走完整链路;来款自动销账请用 applyNetBalanceClear)
  245. *
  246. * $options['paymentSettleOnly'] 只减订单待结
  247. * $options['skipCashMoney'] 现金已在 rechargeBalance 记过
  248. */
  249. public static function confirmClear($clear, $payWay, $options = [])
  250. {
  251. if ($clear->status == 2) {
  252. util::fail('已结过账了');
  253. }
  254. if ($clear->status == 3) {
  255. util::fail('已取消了');
  256. }
  257. $current = time();
  258. if ($current >= strtotime($clear->deadline)) {
  259. util::fail('已过期,请手动取消');
  260. }
  261. $clear->payWay = $payWay;
  262. $clear->payTime = date("Y-m-d H:i:s");
  263. $clear->status = 2;
  264. // 结清时同步 onlinePay;显式传入 not(1) 时不得被微信/支付宝兜底覆盖
  265. if (array_key_exists('onlinePay', $options) && is_numeric($options['onlinePay'])) {
  266. $clear->onlinePay = intval($options['onlinePay']);
  267. } elseif (in_array(intval($payWay), [
  268. intval(dict::getDict('payWay', 'wxPay')),
  269. intval(dict::getDict('payWay', 'alipay')),
  270. ], true)) {
  271. $clear->onlinePay = intval(dict::getDict('onlinePay', 'yes'));
  272. }
  273. $clear->save();
  274. $clearId = $clear->id ?? 0;
  275. $customId = $clear->customId ?? 0;
  276. $orderSn = $clear->orderSn ?? '';
  277. $customName = $clear->customName ?? '';
  278. $custom = CustomClass::getLockById($customId);
  279. if (empty($custom)) {
  280. util::fail('没有找到客户');
  281. }
  282. $ghsId = $custom->ghsId;
  283. $ghs = GhsClass::getLockById($ghsId);
  284. if (empty($ghs)) {
  285. util::fail('没有找到供货商');
  286. }
  287. // true=净余额模型下的销单;false=旧链路(仍调 clearDebtAmountReduce,complete=1 当场结清等)
  288. $paymentSettleOnly = !empty($options['paymentSettleOnly']);
  289. $orderSettleRows = OrderCgClearClass::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
  290. if (empty($orderSettleRows)) {
  291. util::fail('数据错误');
  292. }
  293. $clearTime = date("Y-m-d H:i:s");
  294. $totalSettled = '0.00';
  295. // 手工结账单可能 actPrice < 明细合计(抹零);合并后销单以 actPrice 为本次付款上限
  296. $settleBudget = bcadd((string)($clear->actPrice ?? '0'), '0', 2);
  297. if ($paymentSettleOnly && bccomp($settleBudget, '0', 2) <= 0) {
  298. $settleBudget = bcadd((string)($clear->prePrice ?? '0'), '0', 2);
  299. }
  300. foreach ($orderSettleRows as $os) {
  301. $orderId = intval($os->orderId ?? 0);
  302. $cgId = intval($os->cgId ?? 0);
  303. $mayClear = bcadd((string)($os->amount ?? 0), '0', 2);
  304. if ($orderId <= 0 || bccomp($mayClear, '0', 2) <= 0) {
  305. continue;
  306. }
  307. if ($paymentSettleOnly && bccomp($settleBudget, '0', 2) > 0) {
  308. $budgetLeft = bcsub($settleBudget, $totalSettled, 2);
  309. if (bccomp($budgetLeft, '0', 2) <= 0) {
  310. break;
  311. }
  312. if (bccomp($mayClear, $budgetLeft, 2) > 0) {
  313. $mayClear = $budgetLeft;
  314. }
  315. }
  316. $order = OrderClass::getById($orderId, true);
  317. if (empty($order) || ($order->debt ?? 0) == 0) {
  318. continue;
  319. }
  320. // 按 OrderCgClear.amount 部分或足额减少待结,不是一律置 remainDebtPrice=0
  321. $newRemain = bcsub((string)($order->remainDebtPrice ?? 0), $mayClear, 2);
  322. $orderUpdate = [
  323. 'remainDebtPrice' => bccomp($newRemain, '0', 2) > 0 ? $newRemain : '0.00',
  324. 'clearTime' => $clearTime,
  325. ];
  326. if (bccomp($newRemain, '0', 2) <= 0) {
  327. $orderUpdate['debt'] = 0;
  328. $orderUpdate['clearId'] = $clearId;
  329. }
  330. OrderClass::updateById($orderId, $orderUpdate);
  331. // 批发单销账后立即按 xhGhsOrder 结果写回 xhCg,与充值 FIFO 同一事务内完成
  332. self::syncHdPurchaseAfterOrderClear($orderId, $cgId, $clearId, $clearTime);
  333. $totalSettled = bcadd($totalSettled, $mayClear, 2);
  334. $os->amount = $mayClear;
  335. $os->status = 1;
  336. $os->save();
  337. }
  338. if (!$paymentSettleOnly) {
  339. // 未合并或旧当场结清:仍减少 debtAmount 字段(合并后新业务勿走此分支)
  340. $amount = $clear->prePrice ?? 0;
  341. CustomClass::clearDebtAmountReduce($custom, $ghs, $amount, $clear);
  342. \bizHd\ghs\classes\GhsClass::clearDebtAmountReduce($ghs, $custom, $amount, $clear);
  343. } else {
  344. // 挂账已在 balance,只根据净余额刷新欠款标记;扣款在 clearConsumeBalance
  345. $amount = $totalSettled;
  346. $custom->isDebt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
  347. $custom->save(false, ['isDebt']);
  348. $ghs->debt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? 2 : 1;
  349. $ghs->save(false, ['debt']);
  350. }
  351. //现金余额增加(充值销账链路里现金已在 rechargeBalance 记过,需 skipCashMoney)
  352. $skipCashMoney = !empty($options['skipCashMoney']);
  353. if (!$skipCashMoney && $payWay == dict::getDict('payWay', 'cash')) {
  354. $mainId = $ghs->mainId ?? 0;
  355. $main = MainClass::getLockById($mainId);
  356. if (empty($main)) {
  357. util::fail('没有商家信息');
  358. }
  359. $moneyBalance = bcadd($main->money, $amount, 2);
  360. $main->money = $moneyBalance;
  361. $main->save();
  362. $moneyEvent = $customName . "现金结账" . floatval($amount) . "元,单号 {$orderSn})";
  363. $moneyRemark = '';
  364. $capitalType = dict::getDict('capitalType', 'ghsXsClear', 'id');
  365. $change = [
  366. 'relateId' => $clearId,
  367. 'amount' => $amount,
  368. 'balance' => $moneyBalance,
  369. 'io' => 1,
  370. 'mainId' => $mainId,
  371. 'capitalType' => $capitalType,
  372. 'ptStyle' => dict::getDict('ptStyle', 'ghs'),
  373. 'event' => $moneyEvent,
  374. 'remark' => $moneyRemark,
  375. ];
  376. ShopMoneyChangeClass::addData($change);
  377. }
  378. OrderCgClearClass::markDoneByClearId($clearId);
  379. }
  380. /**
  381. * 批发订单销账后同步花店采购单待结(充值 FIFO、结账单确认等)。
  382. * 以销账后的 xhGhsOrder 为准写回 xhCg,避免两边各自加减导致 remainDebtPrice 不一致。
  383. *
  384. * @param int $orderId 批发销售单 id(xhGhsOrder.id)
  385. * @param int $cgId 结账单明细上的采购单 id,缺失时按 saleId 回查 xhCg
  386. * @param int $clearId 结账单 id
  387. * @param string $clearTime 销账时间
  388. */
  389. protected static function syncHdPurchaseAfterOrderClear($orderId, $cgId, $clearId, $clearTime)
  390. {
  391. $orderId = intval($orderId);
  392. $cgId = intval($cgId);
  393. $clearId = intval($clearId);
  394. if ($orderId <= 0) {
  395. return;
  396. }
  397. $order = OrderClass::getById($orderId, true);
  398. if (empty($order)) {
  399. noticeUtil::push("批发销账后同步采购单失败:未找到销售单 orderId:{$orderId} clearId:{$clearId}", '15280215347');
  400. return;
  401. }
  402. $purchase = null;
  403. if ($cgId > 0) {
  404. $purchase = PurchaseClass::getById($cgId, true);
  405. }
  406. if (empty($purchase)) {
  407. $orderSn = $order->orderSn ?? '';
  408. noticeUtil::push(
  409. "批发销账后同步采购单失败:未找到采购单 orderId:{$orderId} orderSn:{$orderSn} cgId:{$cgId} clearId:{$clearId}",
  410. '15280215347'
  411. );
  412. return;
  413. }
  414. $orderRemain = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
  415. $orderDebtFlag = intval($order->debt ?? 0);
  416. $orderDebtPrice = bcadd((string)($order->debtPrice ?? $order->orderPrice ?? '0'), '0', 2);
  417. $saveFields = ['remainDebtPrice', 'clearTime', 'debt'];
  418. $purchase->remainDebtPrice = bccomp($orderRemain, '0', 2) > 0 ? $orderRemain : '0.00';
  419. $purchase->clearTime = $clearTime;
  420. if ($orderDebtFlag === OrderClass::DEBT_YES) {
  421. $purchase->debt = PurchaseClass::DEBT_YES;
  422. } else {
  423. $purchase->debt = PurchaseClass::DEBT_NO;
  424. $orderClearId = intval($order->clearId ?? 0);
  425. if ($orderClearId > 0) {
  426. $purchase->clearId = $orderClearId;
  427. } elseif ($clearId > 0) {
  428. $purchase->clearId = $clearId;
  429. }
  430. $saveFields[] = 'clearId';
  431. }
  432. if (bccomp((string)($purchase->debtPrice ?? '0'), '0', 2) <= 0 && bccomp($orderDebtPrice, '0', 2) > 0) {
  433. $purchase->debtPrice = $orderDebtPrice;
  434. $saveFields[] = 'debtPrice';
  435. }
  436. $purchase->save(false, array_values(array_unique($saveFields)));
  437. }
  438. }