SeedLogic.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <?php
  2. namespace addons\Seed\common\logic;
  3. use addons\Seed\common\enums\SeedEnum;
  4. use addons\Seed\common\models\SeedCapitalStatistics;
  5. use addons\Seed\common\models\SeedEnergyPool;
  6. use addons\Seed\common\models\SeedLog;
  7. use addons\Seed\common\models\SeedOrder;
  8. use addons\Seed\common\models\SeedStoreSetting;
  9. use addons\Seed\common\models\SeedUser;
  10. use addons\Seed\common\models\SeedUserStatistics;
  11. use addons\Store\common\models\Store;
  12. use addons\Store\common\models\StoreCashierOrder;
  13. use addons\Store\common\models\StoreOrder;
  14. use addons\Store\common\models\StoreOrderCredit;
  15. use addons\Store\common\models\StoreVideoOrder;
  16. use common\enums\RabbitMqEnum;
  17. use common\enums\RedisKeyEnum;
  18. use common\enums\StatusEnum;
  19. use common\helpers\LockHelper;
  20. use common\models\mall\MallAddons;
  21. use common\traits\ErrorTrait;
  22. use yii\db\Exception;
  23. use yii\db\Expression;
  24. use yii\helpers\Json;
  25. use Yii;
  26. /**
  27. * Class SeedLogic
  28. * @package addons\Seed\common\logic
  29. */
  30. class SeedLogic
  31. {
  32. use ErrorTrait;
  33. /**
  34. * 支付成功/订单完成处理
  35. * @Author: lun
  36. * @DateTime: 2022/7/27 0027 15:30
  37. * @Copyright: copyright (c) 2021 广东七件事集团
  38. * @param $order_id
  39. * @param $order_status
  40. * @param int $event_pay 事件类型[1=普通订单,2=收银台订单]
  41. * @return bool
  42. */
  43. public static function handle($order_id, $order_status, $event_pay = 1)
  44. {
  45. $trans = Yii::$app->db->beginTransaction();
  46. try {
  47. // 查询订单信息
  48. if ($event_pay == 1) {
  49. $order = StoreOrder::getOne([['id' => $order_id]], true, ['detail']);
  50. } elseif($event_pay == 2) {
  51. $order = StoreCashierOrder::getOne([['id' => $order_id]], true);
  52. } else {
  53. $order = StoreVideoOrder::getOne([['id' => $order_id]], true);
  54. }
  55. if (empty($order)) throw new \Exception('暂无用户订单信息');
  56. $order['order_status'] = $order_status;// 订单状态赋值
  57. $order['event_pay'] = $event_pay;
  58. if ($event_pay == 1) {
  59. $pay_money = $order['pay_money'] - $order['shipping_money'];// 支付金额-运费
  60. } else {
  61. $pay_money = $order['pay_money'];// 实付金额
  62. }
  63. if ($pay_money <= 0) {
  64. $trans->commit();
  65. return true;
  66. }
  67. $percent = $start_percent = $end_percent = 0;// 获取种子金数量百分比
  68. // 获取基础设置内容
  69. $config = Yii::$app->services->setting->addons->get($order['mall_id'], SeedEnum::ADDONS_NAME, 'basic');
  70. // 不开启直接退出
  71. if ($config['seed_status'] == 0) {
  72. $trans->commit();
  73. return true;
  74. }
  75. // 判断商家是否参与种子金活动
  76. $no_partake_stores = empty($config['no_partake_stores']) ? [] : Json::decode($config['no_partake_stores'], true);
  77. if ($no_partake_stores && in_array($order['store_id'], $no_partake_stores)) {
  78. $trans->commit();
  79. return true;
  80. }// 不参与直接退出
  81. // 检查数据库中是否已存在该订单记录,存在则不插入避免资金叠加
  82. $has_order = SeedOrder::find()->where(['order_id' => $order_id, 'event_pay' => $event_pay, 'status' => StatusEnum::ENABLED])->asArray()->one();
  83. if ($has_order) {
  84. $trans->commit();
  85. return true;
  86. }
  87. // 根据订单实付金额获取可发的种子金数量
  88. $quota_setting = empty($config['quota_setting']) ? [] : Json::decode($config['quota_setting'], true);
  89. if (empty($quota_setting)) {
  90. $trans->commit();
  91. return true;
  92. }// 无设置种子金数量额度直接退出
  93. // 循环获取种子金比例
  94. srand();
  95. foreach ($quota_setting as $item) {
  96. if ($item['start_pay_money'] <= $pay_money && $item['end_pay_money'] >= $pay_money) {
  97. $start_percent = $item['start_percent'];
  98. $end_percent = $item['end_percent'];
  99. $percent = rand($start_percent, $end_percent);
  100. break;
  101. }
  102. }
  103. $seed_num = floor($pay_money * $percent / 100);// 种子金数量
  104. if (empty($seed_num)) {// 获取的数量为0直接退出
  105. $trans->commit();
  106. return true;
  107. }
  108. // 检查订单是否是赊账订单
  109. $check_order = StoreOrderCredit::find()->where(['order_id' => $order['id']])->asArray()->one();
  110. // 计算存入能量池金额
  111. $ad_pool_percent = $event_pay == 3 ? $config['ad_percent'] : $config['pool_percent'];
  112. $join_pool_money = sprintf("%.2f",substr(sprintf("%.3f", ($pay_money * $ad_pool_percent / 100)), 0, -1));// 不四舍五入保留两位小数
  113. // 额外参数
  114. $extra = [
  115. 'join_pool_money' => $join_pool_money,
  116. 'desc' => '店铺消费',
  117. 'capital_type' => SeedEnum::LOG_CONSUME,
  118. 'setting' => Json::encode(['percent' => $percent, 'ad_pool_percent' => $ad_pool_percent, 'multiple' => $config['multiple'], 'quota_setting' => $config['quota_setting']]),
  119. ];
  120. // 消费者获得种子金
  121. self::ConsumeGetSeed($order['user_id'], $seed_num, $order, $config, $extra, $check_order, 1);
  122. // 获取店铺绑定的会员id
  123. $store_info = Store::find()->select('user_id')->where(['id' => $order['store_id']])->asArray()->one();
  124. // 获取店长种子金数量、价值、单价计算
  125. $store_seed_num = self::getSeedCalculate($order['mall_id'], $order['store_id'], $pay_money);
  126. // 店长获得种子金 - 店长发布广告不参与
  127. if ($event_pay <= 2) {
  128. self::ConsumeGetSeed($store_info['user_id'], $store_seed_num, $order, $config, $extra, $check_order, 2);
  129. }
  130. // 种子金/能量池累计
  131. $total_seed_num = $seed_num + $store_seed_num;
  132. $res = SeedCapitalStatistics::saveStatistics($order['mall_id'], [
  133. 'total_seed_num' => $total_seed_num,
  134. 'total_energy_pool' => $join_pool_money,
  135. 'cumulative_energy_pool' => $join_pool_money,
  136. ]);
  137. if ($res === false) throw new \Exception(SeedUserStatistics::getStaticError());
  138. $trans->commit();
  139. return true;
  140. } catch (\Exception $e) {
  141. $trans->rollBack();
  142. self::setStaticError('第'.$e->getLine().'行,原因:'.$e->getMessage());
  143. return false;
  144. }
  145. }
  146. /**
  147. * 能量池释放 - N个小时释放一次
  148. * @Author: lun
  149. * @DateTime: 2022/8/1 0001 16:45
  150. * @Copyright: copyright (c) 2021 广东七件事集团
  151. * @param $mall_id
  152. */
  153. public static function release($mall_id)
  154. {
  155. $trans = Yii::$app->db->beginTransaction();
  156. $lock_key = RedisKeyEnum::suffix(SeedEnum::LOCK_POOL , $mall_id);
  157. try {
  158. $time = time();
  159. // 获取释放的设置
  160. $config = Yii::$app->services->setting->addons->get($mall_id, SeedEnum::ADDONS_NAME, 'basic');
  161. if (empty($config)) {
  162. $trans->commit();
  163. return true;
  164. }
  165. // 获取能量池数据
  166. $pool = SeedCapitalStatistics::findOne(['mall_id' => $mall_id, 'status' => StatusEnum::ENABLED]);
  167. // 判断是否已到释放时间 - 时间误差在2分钟内
  168. if ($pool->last_release_at + $config['release_cycle'] * 3600 > $time + 120) {
  169. $trans->commit();
  170. return true;
  171. }
  172. // 加锁
  173. LockHelper::lock($lock_key, SeedEnum::LOCK_POOL_UNIQID, 30, 60);
  174. // 获取未纳入能量池释放的能量池总额
  175. $frozen_seed_num = SeedOrder::find()->where(['mall_id' => $mall_id, 'seed_status' => SeedEnum::SEED_ADD, 'status' => StatusEnum::ENABLED])->andWhere(['>', 'can_send_at', $time])->sum("seed_num") ?? 0;
  176. // 已停止增长的种子金总数
  177. $stop_seed_num = SeedOrder::find()->where(['mall_id' => $mall_id, 'seed_status' => SeedEnum::SEED_STOP, 'status' => StatusEnum::ENABLED])->sum("seed_num") ?? 0;
  178. // 当前收取种子金数量 = 累计 - 已收取 - 退款 - 坏账 - 未到释放时间的种子金 - 已停止增长的种子金
  179. $seed_num = $pool->total_seed_num - $pool->collect_seed_num - $pool->refund_seed_num - $pool->bad_seed_num - $frozen_seed_num - $stop_seed_num;
  180. if ($seed_num == 0) {// 没有可收取的种子金则直接退出
  181. $trans->commit();
  182. // 解锁
  183. LockHelper::uLock($lock_key, SeedEnum::LOCK_POOL_UNIQID);
  184. return true;
  185. } elseif ($seed_num < 0) {
  186. throw new \Exception("释放的种子金数量小于零:总数={$pool->total_seed_num},已收取={$pool->collect_seed_num},退款={$pool->refund_seed_num},坏账={$pool->bad_seed_num},未到释放时间总数={$frozen_seed_num}");
  187. }
  188. $total_energy_pool = $pool->total_energy_pool;// 当前能量池总额
  189. // 可释放的单价 = 当前能量池总额 / 当前收取种子金数量;保留两位小数不四舍五入
  190. $unit_price = sprintf("%.2f",substr(sprintf("%.3f", ($total_energy_pool / $seed_num)), 0, -1));
  191. $release_energy_pool = $unit_price * $seed_num;// 本次释放能量
  192. if ($release_energy_pool <= 0) $release_energy_pool = $unit_price = 0;// 本次释放的金不足则改为不释放
  193. if ($release_energy_pool > $pool->total_energy_pool) throw new \Exception("释放的能量池额度大于现有的能量池总额:释放={$release_energy_pool},现有={$pool->total_energy_pool}");
  194. $pool->total_energy_pool -= $release_energy_pool;
  195. $pool->release_energy_pool += $release_energy_pool;
  196. $pool->release_num += 1;
  197. $pool->last_release_at = $time;
  198. if (!$pool->save()) throw new Exception($pool->getErrorMessage());
  199. // 增加释放记录
  200. $pool_log = SeedEnergyPool::setData([
  201. 'mall_id' => $pool->mall_id,
  202. 'release_num' => $pool->release_num,
  203. 'total_seed_num' => $seed_num,
  204. 'release_energy_pool' => $release_energy_pool,
  205. 'unit_price' => $unit_price,
  206. 'release_at' => $time,
  207. ]);
  208. if ($pool_log === false) throw new \Exception(SeedEnergyPool::getStaticError());
  209. $trans->commit();
  210. // 释放能量跟单价大于0则执行后续队列
  211. if ($release_energy_pool > 0 && $unit_price > 0) {
  212. // 异步队列进行每个订单的能量释放
  213. Yii::$app->services->rabbitMq->push(RabbitMqEnum::EXCHANGE_TASK, RabbitMqEnum::TASK_QUEUE, [
  214. 'pool_id' => $pool_log['id'],
  215. 'mall_id' => $mall_id,
  216. 'unit_price' => $unit_price,
  217. 'release_at' => $time,
  218. ], self::class, 'handleSync');
  219. }
  220. return true;
  221. } catch (\Exception $e) {
  222. self::setStaticError('第'.$e->getLine().'行,原因:'.$e->getMessage());
  223. LockHelper::uLock($lock_key, SeedEnum::LOCK_POOL_UNIQID);
  224. $trans->rollBack();
  225. return false;
  226. }
  227. }
  228. /**
  229. * 异步处理发放种子金价值
  230. * @Author: lun
  231. * @DateTime: 2022/8/3 0003 10:09
  232. * @Copyright: copyright (c) 2021 广东七件事集团
  233. * @param $params
  234. */
  235. public static function handleSync($params)
  236. {
  237. $trans = Yii::$app->db->beginTransaction();
  238. $lock_key = RedisKeyEnum::suffix(SeedEnum::LOCK_POOL , $params['mall_id']);
  239. try {
  240. $part = 1;
  241. $page_size = 1000;
  242. $time = time();
  243. // 获取基础配置信息
  244. $config = Yii::$app->services->setting->addons->get($params['mall_id'], SeedEnum::ADDONS_NAME, 'basic');
  245. // 获取此次发放的池记录
  246. $pool = SeedEnergyPool::findOne(['id' => $params['pool_id']]);
  247. if (empty($pool)) {
  248. $trans->commit();
  249. LockHelper::uLock($lock_key, SeedEnum::LOCK_POOL_UNIQID);
  250. return true;
  251. }
  252. while ($list = SeedOrder::find()
  253. ->where(['mall_id' => $params['mall_id'], 'seed_status' => SeedEnum::SEED_ADD,'status' => StatusEnum::ENABLED])
  254. ->andWhere(['<=','can_send_at',$params['release_at']])
  255. ->offset(($part - 1) * $page_size)->limit($page_size)->asArray()->all()) {
  256. $seed_users = $auto_collect = $handle_credit = [];
  257. foreach ($list as $item) {
  258. $now_unit_price = $item['unit_price'] + $params['unit_price'];
  259. $add_seed_cost = sprintf("%.2f",substr(sprintf("%.3f", ($params['unit_price'] * $item['seed_num'])), 0, -1));// 总价值
  260. $seed_cost = $item['seed_cost'] + $add_seed_cost;
  261. // 修改用户的种子金价值
  262. $seed_users[$item['user_id']] = empty($seed_users[$item['user_id']]) ? $add_seed_cost : $seed_users[$item['user_id']] + $add_seed_cost;
  263. // 修改订单的单价、总价值
  264. SeedOrder::updateAll([
  265. 'unit_price' => $now_unit_price,
  266. 'seed_cost' => $seed_cost,
  267. 'increase_num' => $item['increase_num'] + 1,
  268. 'last_send_at' => $time,
  269. 'updated_at' => $time,
  270. ],['id' => $item['id']]);
  271. // 增加种子金变动记录
  272. $rows[] = [
  273. 'seed_order_id' => $item['id'],
  274. 'mall_id' => $item['mall_id'],
  275. 'store_id' => $item['store_id'],
  276. 'user_id' => $item['user_id'],
  277. 'order_id' => $item['order_id'],
  278. 'event_pay' => $item['event_pay'],
  279. 'seed_num' => $item['seed_num'],
  280. 'seed_cost' => $seed_cost,
  281. 'change_unit_price' => $item['unit_price'],
  282. 'after_unit_price' => $now_unit_price,
  283. 'desc' => '能量池释放',
  284. 'capital_type' => SeedEnum::LOG_RELEASE,
  285. 'pool_id' => $params['pool_id'],
  286. 'created_at' => $time,
  287. 'updated_at' => $time,
  288. ];
  289. // 检查是否是赊账订单
  290. if ($item['is_credit'] == StatusEnum::DISABLED) {
  291. // 判断满N倍自动释放到余额
  292. if ($now_unit_price / $item['basic_unit_price'] >= $config['multiple']) $auto_collect[$item['event_pay']][] = $item['id'];
  293. } else {
  294. // 判断增长金额 >= 赊账金额
  295. if ($seed_cost >= $item['credit_money']) {
  296. // 消费者处理 - 全部自动收取
  297. if ($item['identity'] == 1) {
  298. // 未还款订单无论是否达到倍数都将自动收取
  299. if ($item['is_able_collect'] == StatusEnum::DISABLED) {
  300. $auto_collect[$item['event_pay']][] = $item['id'];
  301. } else {
  302. // 已还款,当倍数大于设定倍数是将自动收取
  303. if ($now_unit_price / $item['basic_unit_price'] >= $config['multiple']) $auto_collect[$item['event_pay']][] = $item['id'];
  304. }
  305. } else {
  306. // 达到倍数则设置自动收取
  307. if ($now_unit_price / $item['basic_unit_price'] >= $config['multiple']) $auto_collect[$item['event_pay']][] = $item['id'];
  308. }
  309. } else {
  310. // 增长金额 < 赊账金额
  311. if ($item['identity'] == 1) {
  312. // 达到倍数,但是价值又不足以还款,则将此订单设为坏账(冻结)订单
  313. if ($now_unit_price / $item['basic_unit_price'] >= $config['multiple']) {
  314. // 判断是否属于已还款类型,未还款插入冻结,已还款则自动收取
  315. if ($item['is_able_collect'] == StatusEnum::DISABLED) {
  316. $bad_order_arr[] = $item['id'];
  317. } else {
  318. $auto_collect[$item['event_pay']][] = $item['id'];
  319. }
  320. }
  321. } else {
  322. // 达到倍数则设置自动收取
  323. if ($now_unit_price / $item['basic_unit_price'] >= $config['multiple']) $auto_collect[$item['event_pay']][] = $item['id'];
  324. }
  325. }
  326. }
  327. }
  328. // 修改用户种子金的价值
  329. if ($seed_users) {
  330. foreach ($seed_users as $user_id => $seed_cost) {
  331. SeedUser::updateAll([
  332. 'total_seed_cost' => new Expression('total_seed_cost+'.$seed_cost),
  333. 'updated_at' => $time,
  334. ], ['user_id' => $user_id, 'status' => StatusEnum::ENABLED]);
  335. }
  336. }
  337. // 批量插入种子金变动记录
  338. if (!empty($rows)) {
  339. $field = ['seed_order_id','mall_id','store_id','user_id','order_id','event_pay','seed_num','seed_cost','change_unit_price','after_unit_price','desc','capital_type','pool_id','created_at','updated_at'];
  340. Yii::$app->db->createCommand()->batchInsert(SeedLog::tableName(), $field, $rows)->execute();
  341. }
  342. // 达到N倍后自动收取种子金
  343. if ($auto_collect) {
  344. foreach ($auto_collect as $type => $seed_order_ids) {
  345. $handle_credit = SeedOrder::batchCollect($params['mall_id'], $seed_order_ids, $type);
  346. if ($handle_credit === false) throw new \Exception(SeedOrder::getStaticError());
  347. }
  348. }
  349. // 坏账订单处理
  350. if ($bad_order_arr) {
  351. $res = SeedOrder::badOrderHandle($params['mall_id'], $bad_order_arr);
  352. if ($res === false) throw new \Exception(SeedOrder::getStaticError());
  353. }
  354. // 赊账订单处理
  355. if ($handle_credit && MallAddons::isInstall($params['mall_id'], 'Store')) {
  356. foreach ($handle_credit as $type => $credit_order) {
  357. foreach ($credit_order as $order_id) {
  358. // 调用门店订单处理
  359. StoreOrderCredit::repayment($order_id,$type);
  360. }
  361. }
  362. }
  363. $part++;
  364. }
  365. // 修改释放状态
  366. $pool->release_status = SeedEnum::RELEASE_SUCCESS;
  367. if (!$pool->save()) throw new Exception($pool->getErrorMessage());
  368. $trans->commit();
  369. // 解锁
  370. LockHelper::uLock($lock_key, SeedEnum::LOCK_POOL_UNIQID);
  371. return true;
  372. } catch (\Exception $e) {
  373. $trans->rollBack();
  374. // 解锁
  375. LockHelper::uLock($lock_key, SeedEnum::LOCK_POOL_UNIQID);
  376. SeedCapitalStatistics::handleReleaseFail($params, '第'.$e->getLine().'行,原因:'.$e->getMessage());
  377. return true;
  378. }
  379. }
  380. /**
  381. * 用户消费获得种子金
  382. * @Author: lun
  383. * @DateTime: 2022/8/22 0022 14:30
  384. * @Copyright: copyright (c) 2021 广东七件事集团
  385. * @param $user_id
  386. * @param $seed_num
  387. * @param $order
  388. * @param $config
  389. * @param $extra
  390. * @param $check_order
  391. * @param $identity
  392. * @throws \Exception
  393. */
  394. public static function ConsumeGetSeed($user_id, $seed_num, $order, $config, $extra, $check_order, $identity = 1)
  395. {
  396. // 保存用户的种子金数量
  397. if ($seed_num) {
  398. $res = SeedOrder::consumeOrder([
  399. 'mall_id' => $order['mall_id'],
  400. 'store_id' => $order['store_id'],
  401. 'user_id' => $user_id,
  402. 'identity' => $identity,
  403. 'order_id' => $order['id'],
  404. 'order_no' => $order['order_no'],
  405. 'event_pay' => $order['event_pay'],
  406. 'goods_name' => $order['event_pay'] == 1 ? $order['detail'][0]['goods_name'] : $order['event_pay'] == 2 ? '收银台消费' : '发布广告',
  407. 'seed_num' => $seed_num,
  408. 'basic_unit_price' => $config['basic_unit_price'],
  409. 'unit_price' => $config['basic_unit_price'],
  410. 'seed_status' => SeedEnum::SEED_ADD,
  411. 'join_pool_money' => $identity == 1 ? $extra['join_pool_money'] : 0,// 消费者才增加能量池金额
  412. 'can_send_at' => time() + $config['release_cycle'] * 3600,
  413. 'credit_money' => empty($check_order) ? 0 : $check_order['credit_money'],
  414. 'is_credit' => empty($check_order) ? StatusEnum::DISABLED : StatusEnum::ENABLED,
  415. 'is_able_collect' => empty($check_order) ? StatusEnum::ENABLED : $identity == 1 ? ($order['event_pay'] <= 2 ? StatusEnum::DISABLED : StatusEnum::ENABLED) : StatusEnum::ENABLED,
  416. ], $extra);
  417. if ($res === false) throw new \Exception(SeedOrder::getStaticError());
  418. // 用户数据统计 - 为了防止数据并发,使用updateAllCounters做累加
  419. $res = SeedUserStatistics::setData([
  420. 'mall_id' => $order['mall_id'],
  421. 'store_id' => $order['store_id'],
  422. 'user_id' => $user_id,
  423. ], ['pay_order_num' => 1, 'seed_num' => $seed_num]);
  424. if ($res === false) throw new \Exception(SeedUserStatistics::getStaticError());
  425. }
  426. }
  427. /**
  428. * 计算店长可得种子金
  429. * @Author: lun
  430. * @DateTime: 2022/8/22 0022 14:23
  431. * @Copyright: copyright (c) 2021 广东七件事集团
  432. * @param $mall_id
  433. * @param $store_id
  434. * @param $pay_money
  435. * @return false|float
  436. */
  437. public static function getSeedCalculate($mall_id, $store_id, $pay_money)
  438. {
  439. // 店铺独立设置
  440. $percent = SeedStoreSetting::find()->select('percent')->where(['mall_id' => $mall_id, 'store_id' => $store_id, 'status' => StatusEnum::ENABLED])->scalar();
  441. if (empty($percent)) {
  442. $percent = Yii::$app->services->setting->addons->get($mall_id, SeedEnum::ADDONS_NAME, 'store.percent') ?? 0;
  443. }
  444. $seed_num = empty($percent) ? 0 : floor($pay_money * $percent / 100);// 种子金数量
  445. return $seed_num;
  446. }
  447. }