UserCoinController.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <?php
  2. namespace app\api\controller\coin;
  3. use app\admin\model\user\User;
  4. use app\admin\model\user\UserBill;
  5. use app\models\coin\UserCoinTransfer;
  6. use tw\redis\UserRds;
  7. use app\models\store\StoreOrderCartInfo;
  8. use app\models\system\DictCoin;
  9. use app\models\user\UserCoin;
  10. use app\Request;
  11. use crmeb\services\{UtilService, JsonService};
  12. use think\facade\Config;
  13. use think\facade\Cache;
  14. // 精度相关的函数
  15. // 转换为 string, 不同于 strval(),这个对于极小 float 不会使用科学计数法表示
  16. function twstr($val, $scale = 2): string
  17. {
  18. return number_format($val, $scale, '.', '');
  19. }
  20. function twadd($l, $r, $scale = 2): string
  21. {
  22. return bcadd(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  23. }
  24. function twsub($l, $r, $scale = 2): string
  25. {
  26. return bcsub(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  27. }
  28. function twmul($l, $r, $scale = 2): string
  29. {
  30. return bcmul(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  31. }
  32. function twdiv($l, $r, $scale = 2): string
  33. {
  34. return bcdiv(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  35. }
  36. function twmod($l, $r, $scale = 2): string
  37. {
  38. return bcmod(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  39. }
  40. function twcomp($l, $r, $scale = 2): string
  41. {
  42. return bccomp(twstr($l, $scale + 2), twstr($r, $scale + 2), $scale);
  43. }
  44. class UserCoinController
  45. {
  46. /**
  47. * @api {get} /coin/status 挖矿当前状态
  48. * @apiName GetCoinStatus
  49. * @apiGroup User.Coin
  50. *
  51. */
  52. public function status(Request $request)
  53. {
  54. // 是否开启挖矿
  55. $symbol = Config::get('app.mining_symbo');
  56. //
  57. $coinInfo = Cache::get($symbol);
  58. if (!$coinInfo) {
  59. $coinInfo = DictCoin::where('symbol', $symbol)->find();
  60. if (!$coinInfo) {
  61. return app('json')->failed('活动临时关闭,请联系客服');
  62. }
  63. $coinInfo = $coinInfo->toArray();
  64. Cache::set($symbol, $coinInfo);
  65. }
  66. // print_r($coinInfo);
  67. //
  68. $defStatus = [
  69. 'boot' => 0,
  70. 'stop' => 0,
  71. 'progress' => 0,
  72. 'symbol' => $symbol,
  73. 'icon' => $coinInfo['icon'],
  74. 'price' => $coinInfo['price'],
  75. 'total' => 0,
  76. 'step' => 0,
  77. 'ts' => 0,
  78. ];
  79. // 沒配置挖什麼
  80. if (!$symbol) {
  81. warnlog('mining disabled: $symbol');
  82. return app('json')->successful($defStatus);
  83. }
  84. // 獲取用戶進度
  85. $uid = $request->uid();
  86. $rds = new UserRds();
  87. $mymining = json_decode($rds->hget($uid, UserRds::FIELD_MINING), true);
  88. if (!$mymining) {
  89. $mymining = $defStatus;
  90. return app('json')->successful($defStatus);
  91. }
  92. //
  93. if ($mymining['progress'] > 0.0) {
  94. $mymining = $this->calcMining($uid, $coinInfo, $mymining);
  95. $rds->hset($uid, UserRds::FIELD_MINING, json_encode($mymining));
  96. } else { // 本次挖矿已经结束
  97. $mymining['total'] = UserCoin::where('uid', $uid)->where('symbol', $symbol)->value('balance') ?? 0.0;
  98. $mymining['symbol'] = $symbol;
  99. $mymining['icon'] = $coinInfo['icon'];
  100. $mymining['price'] = $coinInfo['price'];
  101. $mymining['step'] = self::get_step();
  102. }
  103. return app('json')->successful([
  104. 'symbol' => $mymining['symbol'],
  105. 'icon' => $mymining['icon'],
  106. 'price' => $mymining['price'],
  107. 'step' => $mymining['step'],
  108. 'progress' => $mymining['progress'],
  109. 'total' => $mymining['total'],
  110. ]);
  111. }
  112. /**
  113. * 根据 $step 返回一个新 $step
  114. * 新的 $step 值为围绕 旧 $step 周围的随机值,上下波动幅度为 10%
  115. */
  116. protected function floatStep($step)
  117. {
  118. $amp = twdiv($step, 10, 8);
  119. $min = twsub($step, $amp, 8);
  120. $max = twadd($step, $amp, 8);
  121. $distance = twsub($max, $min, 8);
  122. $section = twdiv($distance, 10, 8);
  123. return twmul($section, mt_rand(0, 10), 8) + $min;
  124. }
  125. /**
  126. * 根据上次挖矿状态, 和过去的时长, 计算当前的状态
  127. * @param $p
  128. * @return mixed
  129. */
  130. protected function calcMining($uid, $coinInfo, $p)
  131. {
  132. if (
  133. !isset($p['ts']) ||
  134. !isset($p['boot']) ||
  135. !isset($p['stop']) ||
  136. !isset($p['step']) ||
  137. !isset($p['progress'])
  138. ) {
  139. warnlog('error format.');
  140. return $p;
  141. }
  142. if ($p['progress'] <= 0) {
  143. return $p;
  144. }
  145. $step = $this->floatStep($p['step']);
  146. $now = time();
  147. $secs_passed = $now - $p['ts']; // 从上次到现在
  148. //
  149. if ($now >= $p['stop']) { // 挖矿结束
  150. $secs_remain = $p['stop'] - $p['ts'];
  151. if ($secs_remain < 0) {
  152. $secs_remain = 0;
  153. }
  154. // 本次个数
  155. $count = floatval(twmul($step, $secs_remain, 8));
  156. $p['progress'] += floatval(twadd($p['progress'], $count, 8));
  157. // save to db
  158. UserCoinTransfer::beginTrans();
  159. $r1 = UserCoinTransfer::addMining($uid, $p['order_id'], $p['symbol'], $p['progress']);
  160. $r2 = UserCoin::upsertCoin($uid, $p['symbol'], $p['progress']);
  161. UserCoinTransfer::checkTrans($r1 && $r2);
  162. if (!$r1 || !$r2) {
  163. $amount = $p['progress'];
  164. errlog("user<$uid> save transfer failed, amount<$amount>");
  165. return $p;
  166. }
  167. // -- save to db
  168. $p['ts'] = $now;
  169. $p['total'] = floatval(twadd($p['total'], $p['progress'], 8));
  170. $p['progress'] = 0;
  171. // 更换为可能的新的币种
  172. $p['symbol'] = $coinInfo['symbol'];
  173. $p['icon'] = $coinInfo['icon'];
  174. $p['price'] = $coinInfo['price'];
  175. $p['step'] = self::get_step();
  176. return $p;
  177. }
  178. // 进行中
  179. $count = floatval(twmul($step, $secs_passed, 8));
  180. $p['progress'] = floatval(twadd($p['progress'], $count, 8));
  181. $p['ts'] = $now;
  182. return $p;
  183. }
  184. public static function get_step(): float
  185. {
  186. $secs_unit = Config::get('app.mining_sec_unit');
  187. $reward_unit = Config::get('app.mining_num_per_unit');
  188. return twdiv($reward_unit, $secs_unit, 8);
  189. }
  190. /**
  191. * @api {post} /coin/boot 启动挖矿
  192. * @apiName PostCoinBoot
  193. * @apiGroup User.Coin
  194. *
  195. */
  196. public function boot(Request $request)
  197. {
  198. $uid = $request->uid();
  199. $now = time();
  200. // 是否停止
  201. if (Config::get('activity.mining_stopped')) {
  202. return app('json')->fail('活动已暂停,稍后开启');
  203. }
  204. // 是否开启活动
  205. $symbol = Config::get('app.mining_symbo');
  206. if (!$symbol) {
  207. return app('json')->fail('本活动未开启');
  208. }
  209. // 是否已经开启
  210. $rds = new UserRds();
  211. $mining = json_decode($rds->hget($uid, UserRds::FIELD_MINING), true);
  212. if ($mining) {
  213. if (
  214. isset($mining['progress']) &&
  215. $mining['progress'] > 0.0 &&
  216. isset($mining['boot']) &&
  217. isset($mining['stop']) &&
  218. isset($mining['ts'])
  219. ) {
  220. return app('json')->fail('已启动');
  221. }
  222. }
  223. // 是否有订单
  224. $orderId = StoreOrderCartInfo::getMiningOrderId($uid);
  225. if (!$orderId) {
  226. return app('json')->fail('参加活动下单后可启动,请参看 首页->官方资讯->新手教程');
  227. }
  228. // 标记订单
  229. if (!StoreOrderCartInfo::setMining($orderId)) {
  230. return app('json')->fail('启动失败');
  231. }
  232. // 开启
  233. $step = self::get_step();
  234. $progress = $step;
  235. $icon = DictCoin::getIcon($symbol);
  236. // 已挖总额
  237. $balance = UserCoin::where('uid', $uid)->where('symbol', $symbol)->value('balance') ?? 0.0;
  238. // 单次活动时长
  239. $hours = Config::get('app.mining_time');
  240. if (count($hours) != 2 || $hours[0] > $hours[1]) {
  241. warnlog('app.mining_time config error.');
  242. return app('json')->fail('启动失败,请联系客服');;
  243. }
  244. $hour = random_int($hours[0], $hours[1]);
  245. $stop = $now + $hour * 60 * 60;
  246. $suc = $rds->hset($uid, UserRds::FIELD_MINING, json_encode([
  247. 'boot' => $now,
  248. 'step' => $step,
  249. 'stop' => $stop,
  250. 'symbol' => $symbol,
  251. 'icon' => $icon,
  252. 'price' => 0,
  253. 'progress' => $progress,
  254. 'total' => $balance,
  255. 'ts' => $now + 1,
  256. 'order_id' => $orderId,
  257. ]));
  258. if ($suc != 0 && $suc != 1) {
  259. StoreOrderCartInfo::setMining($orderId, 0);
  260. return app('json')->fail('未成功启动');
  261. }
  262. return app('json')->successful([
  263. 'symbol' => $symbol,
  264. 'icon' => $icon,
  265. 'price' => 0,
  266. 'step' => $progress,
  267. 'progress' => $progress,
  268. 'total' => $balance,
  269. ]);
  270. }
  271. /**
  272. * @api {get} /coin/history 请求转账记录
  273. * @apiName GetCoinHistory
  274. * @apiGroup User.Coin
  275. *
  276. */
  277. public function history(Request $request)
  278. {
  279. [$page, $limit] = UtilService::getMore([
  280. ['page', 1],
  281. ['limit', 20],
  282. ], $request, true);
  283. $uid = $request->uid();
  284. $rows = UserCoinTransfer::getUserTransferred($uid, $page, $limit);
  285. return app('json')->successful($rows);
  286. }
  287. /**
  288. * @api {post} /coin/addr 更新钱包地址
  289. * @apiName PostCoinAddr
  290. * @apiGroup User.Coin
  291. *
  292. */
  293. public function updateAddr(Request $request)
  294. {
  295. list($symbol, $addr) = UtilService::postMore([
  296. ['symbol', ''],
  297. ['addr', ''],
  298. ], $request, true);
  299. if (!$symbol || !$addr) {
  300. return app('json')->fail('参数不可为空');
  301. }
  302. if (!DictCoin::where('symbol', $symbol)->select()->toArray()) {
  303. return app('json')->fail('未找到目标');
  304. }
  305. $uid = $request->uid();
  306. $suc = UserCoin::upsertAddr($uid, $symbol, $addr);
  307. if (!$suc) {
  308. return app('json')->fail('执行失败');
  309. }
  310. return app('json')->successful();
  311. }
  312. /**
  313. * @api {post} /coin/transfer 提现
  314. * @apiName PostCoinTransfer
  315. * @apiGroup User.Coin
  316. *
  317. */
  318. public function transfer(Request $request)
  319. {
  320. list($symbol, $amount) = UtilService::postMore([
  321. ['symbol', ''],
  322. ['amount', 0]
  323. ], $request, true);
  324. $uid = $request->uid();
  325. $user = User::getUserinfos($uid);
  326. if (!$symbol) {
  327. return app('json')->fail('参数不可为空');
  328. }
  329. $meta = DictCoin::where('symbol', $symbol)->find();
  330. if (!$meta) {
  331. return app('json')->fail('未找到币种');
  332. }
  333. $meta = $meta->toArray();
  334. if ($amount < $meta['min_withdrawal']) {
  335. return app('json')->fail('未达到最低限额');
  336. }
  337. $userCoin = UserCoin::where(['uid' => $uid, 'symbol' => $symbol])->find();
  338. if (!$userCoin) {
  339. return app('json')->fail('不适用的用户');
  340. }
  341. $userCoin = $userCoin->toArray();
  342. if ($symbol == 'RMB') {
  343. return $this->_transfer_fiat($user, $amount, $userCoin);
  344. } else {
  345. return $this->_transfer_crypto_coin($amount, $symbol);
  346. }
  347. }
  348. protected function _transfer_crypto_coin($amount, $symbol)
  349. {
  350. return app('json')->fail('转账加密货币,请联系客服');
  351. }
  352. public function _transfer_fiat($user, $amount, $userCoin)
  353. {
  354. $uid = $user['uid'];
  355. $symbol = $userCoin['symbol'];
  356. $amount = floor($amount);
  357. if ($userCoin['balance'] < $amount) {
  358. return app('json')->fail('余额不足');
  359. }
  360. // transfer
  361. UserCoin::beginTrans();
  362. // 扣除
  363. $res1 = UserCoin::where(['uid' => $uid, 'symbol' => $symbol])->dec('balance', $amount)->update();
  364. // 添加佣金
  365. $res3 = UserBill::income(
  366. '钱包转出' . $symbol,
  367. $uid,
  368. 'now_money',
  369. 'brokerage',
  370. $amount,
  371. 0,
  372. twadd($user['brokerage_price'], $amount, 2),
  373. '黑洞星球转入' . $amount . '元'
  374. );
  375. $res4 = User::bcInc($uid, 'brokerage_price', $amount, 'uid');
  376. // 转账记录
  377. $res2 = true; // UserCoinTransfer::withdrawal($uid, $symbol, $userCoin['addr'], $amount);
  378. $ok = $res1 && $res2 && $res3 && $res4;
  379. UserCoin::checkTrans($ok);
  380. if (!$ok) {
  381. return app('json')->fail('执行失败');
  382. }
  383. return app('json')->successful('转出成功');
  384. }
  385. }