OrderClearClass.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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. * 订单 clearId 约定:
  250. * - 足额结清:写真实清账单 id
  251. * - 部分销账且原 clearId=0:写 1,表示「有结账痕迹」(充值消欠 / 跨天售后返充 FIFO 共用)
  252. */
  253. public static function confirmClear($clear, $payWay, $options = [])
  254. {
  255. if ($clear->status == 2) {
  256. util::fail('已结过账了');
  257. }
  258. if ($clear->status == 3) {
  259. util::fail('已取消了');
  260. }
  261. $current = time();
  262. if ($current >= strtotime($clear->deadline)) {
  263. util::fail('已过期,请手动取消');
  264. }
  265. $clear->payWay = $payWay;
  266. $clear->payTime = date("Y-m-d H:i:s");
  267. $clear->status = 2;
  268. // 结清时同步 onlinePay;显式传入 not(1) 时不得被微信/支付宝兜底覆盖
  269. if (array_key_exists('onlinePay', $options) && is_numeric($options['onlinePay'])) {
  270. $clear->onlinePay = intval($options['onlinePay']);
  271. } elseif (in_array(intval($payWay), [
  272. intval(dict::getDict('payWay', 'wxPay')),
  273. intval(dict::getDict('payWay', 'alipay')),
  274. ], true)) {
  275. $clear->onlinePay = intval(dict::getDict('onlinePay', 'yes'));
  276. }
  277. $clear->save();
  278. $clearId = $clear->id ?? 0;
  279. $customId = $clear->customId ?? 0;
  280. $orderSn = $clear->orderSn ?? '';
  281. $customName = $clear->customName ?? '';
  282. $custom = CustomClass::getLockById($customId);
  283. if (empty($custom)) {
  284. util::fail('没有找到客户');
  285. }
  286. $ghsId = $custom->ghsId;
  287. $ghs = GhsClass::getLockById($ghsId);
  288. if (empty($ghs)) {
  289. util::fail('没有找到供货商');
  290. }
  291. // true=净余额模型下的销单;false=旧链路(仍调 clearDebtAmountReduce,complete=1 当场结清等)
  292. $paymentSettleOnly = !empty($options['paymentSettleOnly']);
  293. $orderSettleRows = OrderCgClearClass::getAllByCondition(['clearId' => $clearId], null, '*', null, true);
  294. if (empty($orderSettleRows)) {
  295. util::fail('数据错误');
  296. }
  297. $clearTime = date("Y-m-d H:i:s");
  298. $totalSettled = '0.00';
  299. // 手工结账单可能 actPrice < 明细合计(抹零);合并后销单以 actPrice 为本次付款上限
  300. $settleBudget = bcadd((string)($clear->actPrice ?? '0'), '0', 2);
  301. if ($paymentSettleOnly && bccomp($settleBudget, '0', 2) <= 0) {
  302. $settleBudget = bcadd((string)($clear->prePrice ?? '0'), '0', 2);
  303. }
  304. foreach ($orderSettleRows as $os) {
  305. $orderId = intval($os->orderId ?? 0);
  306. $cgId = intval($os->cgId ?? 0);
  307. $mayClear = bcadd((string)($os->amount ?? 0), '0', 2);
  308. if ($orderId <= 0 || bccomp($mayClear, '0', 2) <= 0) {
  309. continue;
  310. }
  311. if ($paymentSettleOnly && bccomp($settleBudget, '0', 2) > 0) {
  312. $budgetLeft = bcsub($settleBudget, $totalSettled, 2);
  313. if (bccomp($budgetLeft, '0', 2) <= 0) {
  314. break;
  315. }
  316. if (bccomp($mayClear, $budgetLeft, 2) > 0) {
  317. $mayClear = $budgetLeft;
  318. }
  319. }
  320. $order = OrderClass::getById($orderId, true);
  321. if (empty($order) || ($order->debt ?? 0) == 0) {
  322. continue;
  323. }
  324. // 按 OrderCgClear.amount 部分或足额减少待结,不是一律置 remainDebtPrice=0
  325. $newRemain = bcsub((string)($order->remainDebtPrice ?? 0), $mayClear, 2);
  326. $orderUpdate = [
  327. 'remainDebtPrice' => bccomp($newRemain, '0', 2) > 0 ? $newRemain : '0.00',
  328. 'clearTime' => $clearTime,
  329. ];
  330. if (bccomp($newRemain, '0', 2) <= 0) {
  331. $orderUpdate['debt'] = 0;
  332. // 足额结清:写真实清账单 id,便于追溯
  333. $orderUpdate['clearId'] = $clearId;
  334. } elseif (intval($order->clearId ?? 0) === 0) {
  335. // 部分销账也打结账痕迹:clearId=1(有清账单参与过),供跨天售后/已结账判断
  336. // 覆盖:客户详情充值消欠、跨天售后返充余额后的 FIFO 销账等
  337. $orderUpdate['clearId'] = 1;
  338. }
  339. OrderClass::updateById($orderId, $orderUpdate);
  340. // 批发单销账后立即按 xhGhsOrder 结果写回 xhCg,与充值 FIFO 同一事务内完成
  341. self::syncHdPurchaseAfterOrderClear($orderId, $cgId, $clearId, $clearTime);
  342. $totalSettled = bcadd($totalSettled, $mayClear, 2);
  343. $os->amount = $mayClear;
  344. $os->status = 1;
  345. $os->save();
  346. }
  347. if (!$paymentSettleOnly) {
  348. // 未合并或旧当场结清:仍减少 debtAmount 字段(合并后新业务勿走此分支)
  349. $amount = $clear->prePrice ?? 0;
  350. CustomClass::clearDebtAmountReduce($custom, $ghs, $amount, $clear);
  351. \bizHd\ghs\classes\GhsClass::clearDebtAmountReduce($ghs, $custom, $amount, $clear);
  352. } else {
  353. // 挂账已在 balance,只根据净余额刷新欠款标记;扣款在 clearConsumeBalance
  354. $amount = $totalSettled;
  355. $custom->isDebt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? CustomClass::IS_DEBT_YES : CustomClass::IS_DEBT_NO;
  356. $custom->save(false, ['isDebt']);
  357. $ghs->debt = bccomp($custom->balance ?? '0', '0', 2) < 0 ? 2 : 1;
  358. $ghs->save(false, ['debt']);
  359. }
  360. //现金余额增加(充值销账链路里现金已在 rechargeBalance 记过,需 skipCashMoney)
  361. $skipCashMoney = !empty($options['skipCashMoney']);
  362. if (!$skipCashMoney && $payWay == dict::getDict('payWay', 'cash')) {
  363. $mainId = $ghs->mainId ?? 0;
  364. $main = MainClass::getLockById($mainId);
  365. if (empty($main)) {
  366. util::fail('没有商家信息');
  367. }
  368. $moneyBalance = bcadd($main->money, $amount, 2);
  369. $main->money = $moneyBalance;
  370. $main->save();
  371. $moneyEvent = $customName . "现金结账" . floatval($amount) . "元,单号 {$orderSn})";
  372. $moneyRemark = '';
  373. $capitalType = dict::getDict('capitalType', 'ghsXsClear', 'id');
  374. $change = [
  375. 'relateId' => $clearId,
  376. 'amount' => $amount,
  377. 'balance' => $moneyBalance,
  378. 'io' => 1,
  379. 'mainId' => $mainId,
  380. 'capitalType' => $capitalType,
  381. 'ptStyle' => dict::getDict('ptStyle', 'ghs'),
  382. 'event' => $moneyEvent,
  383. 'remark' => $moneyRemark,
  384. ];
  385. ShopMoneyChangeClass::addData($change);
  386. }
  387. OrderCgClearClass::markDoneByClearId($clearId);
  388. }
  389. /**
  390. * 批发订单销账后同步花店采购单待结(充值 FIFO、结账单确认等)。
  391. * 以销账后的 xhGhsOrder 为准写回 xhCg,避免两边各自加减导致 remainDebtPrice 不一致。
  392. *
  393. * @param int $orderId 批发销售单 id(xhGhsOrder.id)
  394. * @param int $cgId 结账单明细上的采购单 id,缺失时按 saleId 回查 xhCg
  395. * @param int $clearId 结账单 id
  396. * @param string $clearTime 销账时间
  397. */
  398. protected static function syncHdPurchaseAfterOrderClear($orderId, $cgId, $clearId, $clearTime)
  399. {
  400. $orderId = intval($orderId);
  401. $cgId = intval($cgId);
  402. $clearId = intval($clearId);
  403. if ($orderId <= 0) {
  404. return;
  405. }
  406. $order = OrderClass::getById($orderId, true);
  407. if (empty($order)) {
  408. noticeUtil::push("批发销账后同步采购单失败:未找到销售单 orderId:{$orderId} clearId:{$clearId}", '15280215347');
  409. return;
  410. }
  411. $purchase = null;
  412. if ($cgId > 0) {
  413. $purchase = PurchaseClass::getById($cgId, true);
  414. }
  415. if (empty($purchase)) {
  416. $orderSn = $order->orderSn ?? '';
  417. noticeUtil::push(
  418. "批发销账后同步采购单失败:未找到采购单 orderId:{$orderId} orderSn:{$orderSn} cgId:{$cgId} clearId:{$clearId}",
  419. '15280215347'
  420. );
  421. return;
  422. }
  423. $orderRemain = bcadd((string)($order->remainDebtPrice ?? '0'), '0', 2);
  424. $orderDebtFlag = intval($order->debt ?? 0);
  425. $orderDebtPrice = bcadd((string)($order->debtPrice ?? $order->orderPrice ?? '0'), '0', 2);
  426. $saveFields = ['remainDebtPrice', 'clearTime', 'debt'];
  427. $purchase->remainDebtPrice = bccomp($orderRemain, '0', 2) > 0 ? $orderRemain : '0.00';
  428. $purchase->clearTime = $clearTime;
  429. if ($orderDebtFlag === OrderClass::DEBT_YES) {
  430. $purchase->debt = PurchaseClass::DEBT_YES;
  431. } else {
  432. $purchase->debt = PurchaseClass::DEBT_NO;
  433. $orderClearId = intval($order->clearId ?? 0);
  434. if ($orderClearId > 0) {
  435. $purchase->clearId = $orderClearId;
  436. } elseif ($clearId > 0) {
  437. $purchase->clearId = $clearId;
  438. }
  439. $saveFields[] = 'clearId';
  440. }
  441. if (bccomp((string)($purchase->debtPrice ?? '0'), '0', 2) <= 0 && bccomp($orderDebtPrice, '0', 2) > 0) {
  442. $purchase->debtPrice = $orderDebtPrice;
  443. $saveFields[] = 'debtPrice';
  444. }
  445. $purchase->save(false, array_values(array_unique($saveFields)));
  446. }
  447. }