UserCoinController.php 12 KB

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