StakingController.java 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. package com.bizzan.bitrade.controller;
  2. import com.bizzan.bitrade.entity.StakingConfig;
  3. import com.bizzan.bitrade.entity.StakingOrder;
  4. import com.bizzan.bitrade.entity.StakingRecord;
  5. import com.bizzan.bitrade.entity.StakingWallet;
  6. import com.bizzan.bitrade.entity.transform.AuthMember;
  7. import com.bizzan.bitrade.service.LocaleMessageSourceService;
  8. import com.bizzan.bitrade.service.MemberWalletService;
  9. import com.bizzan.bitrade.service.StakingService;
  10. import com.bizzan.bitrade.util.MessageResult;
  11. import com.bizzan.bitrade.vo.StakingWalletVo;
  12. import io.swagger.annotations.Api;
  13. import io.swagger.annotations.ApiOperation;
  14. import lombok.extern.slf4j.Slf4j;
  15. import org.springframework.beans.factory.annotation.Autowired;
  16. import org.springframework.data.domain.Page;
  17. import org.springframework.data.redis.core.RedisTemplate;
  18. import org.springframework.kafka.core.KafkaTemplate;
  19. import org.springframework.web.bind.annotation.*;
  20. import java.math.BigDecimal;
  21. import java.util.ArrayList;
  22. import java.util.HashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import static com.bizzan.bitrade.constant.SysConstant.SESSION_MEMBER;
  26. /**
  27. * 用户质押接口,提供质押、查询订单和流水功能
  28. *
  29. * @author auto
  30. * @date 2026-05-17
  31. */
  32. @Api(value = "StakingController", tags = "用户质押接口")
  33. @RestController
  34. @RequestMapping("/staking")
  35. @Slf4j
  36. public class StakingController {
  37. @Autowired
  38. private StakingService stakingService;
  39. @Autowired
  40. private MemberWalletService memberWalletService;
  41. @Autowired
  42. private LocaleMessageSourceService msService;
  43. @Autowired
  44. private KafkaTemplate<String, String> kafkaTemplate;
  45. @Autowired
  46. private RedisTemplate<String, String> redisTemplate;
  47. /**
  48. * 查询现货最新价(Redis ExchangeNew2:latestPrice,与 IDO 闪兑、质押折算同源)
  49. */
  50. @ApiOperation(value = "查询质押币现货价", notes = "从 Redis 读取 coinUnit 对 USDT 价格,如 IBIT → ibitusdt")
  51. @GetMapping("/spot-price")
  52. public MessageResult getSpotPrice(@RequestParam("coinUnit") String coinUnit) {
  53. if (coinUnit == null || coinUnit.trim().isEmpty()) {
  54. return MessageResult.error(msService.getMessage("PARAMETER_ERROR"));
  55. }
  56. BigDecimal price = stakingService.resolveSpotPriceUsdt(coinUnit.trim());
  57. if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) {
  58. return MessageResult.error(msService.getMessage("STAKING_PRICE_UNAVAILABLE"));
  59. }
  60. Map<String, Object> data = new HashMap<>(2);
  61. data.put("coinUnit", coinUnit.trim().toUpperCase());
  62. data.put("price", price.toPlainString());
  63. return MessageResult.success("success", data);
  64. }
  65. /**
  66. * 查询启用中的质押配置列表,供用户选择质押产品
  67. *
  68. * @return 启用中的质押配置
  69. */
  70. @ApiOperation(value = "查询质押配置列表", notes = "获取所有启用中的质押配置,包含锁定天数、最小质押额等信息")
  71. @PostMapping("/config/list")
  72. public MessageResult getConfigList() {
  73. List<StakingConfig> configs = stakingService.getEnabledConfigs();
  74. return MessageResult.success("success", configs);
  75. }
  76. /**
  77. * 获取 ID 最小且启用中的质押配置,供 IDO 预售页默认展示
  78. *
  79. * @return 单条启用配置
  80. */
  81. @ApiOperation(value = "查询默认质押配置", notes = "返回 status=1 且 id 最小的 staking_config,供 IDO 预售页使用")
  82. @PostMapping("/config/min-open")
  83. public MessageResult getMinOpenConfig() {
  84. StakingConfig config = stakingService.getMinIdEnabledConfig();
  85. if (config == null) {
  86. return MessageResult.error(msService.getMessage("STAKING_NO_PRODUCT"));
  87. }
  88. return MessageResult.success("success", config);
  89. }
  90. /**
  91. * 查询当前用户质押钱包余额,返回原始数量及 USDT 折算值
  92. *
  93. * @param member 当前登录用户
  94. * @param coinUnit 币种,如 IBIT
  95. * @return StakingWalletVo:可用、不可用原始数量及各自 USDT 折算
  96. */
  97. @ApiOperation(value = "查询质押钱包余额", notes = "返回指定币种的可用/锁定原始数量及折合 USDT 值")
  98. @PostMapping("/wallet")
  99. public MessageResult getStakingWallet(@SessionAttribute(SESSION_MEMBER) AuthMember member,
  100. @RequestParam String coinUnit) {
  101. if (coinUnit == null || coinUnit.trim().isEmpty()) {
  102. return MessageResult.error(msService.getMessage("PARAMETER_ERROR"));
  103. }
  104. // 1. 查询质押钱包
  105. StakingWallet wallet = memberWalletService.findStakingWallet(member.getId(), coinUnit.trim());
  106. // 2. 从 Redis 读取现货价格(ExchangeNew2:latestPrice,field 格式如 ibitusdt)
  107. java.math.BigDecimal price = stakingService.resolveSpotPriceUsdt(coinUnit.trim());
  108. if (price == null) {
  109. price = java.math.BigDecimal.ZERO;
  110. }
  111. // 3. 构建响应 VO
  112. StakingWalletVo vo = buildStakingWalletVo(member.getId(), wallet, coinUnit.trim(), price);
  113. return MessageResult.success("success", vo);
  114. }
  115. /**
  116. * 构建质押钱包余额 VO,包含原始锁定数量及 USDT 折算值
  117. *
  118. * @param wallet 质押钱包实体(可能为空余额对象)
  119. * @param coinUnit 币种
  120. * @param price 当前 coinUnit/USDT 价格,无价格传 ZERO
  121. * @return StakingWalletVo
  122. */
  123. private StakingWalletVo buildStakingWalletVo(Long memberId, StakingWallet wallet, String coinUnit, java.math.BigDecimal price) {
  124. java.math.BigDecimal locked = wallet.getLockedBalance() != null ? wallet.getLockedBalance() : java.math.BigDecimal.ZERO;
  125. java.math.BigDecimal released = stakingService.getUserReleasedAmount(memberId, coinUnit);
  126. // 有行情则折算 USDT,否则为 0
  127. java.math.BigDecimal lockedUsdt = price.compareTo(java.math.BigDecimal.ZERO) > 0 ? locked.multiply(price) : java.math.BigDecimal.ZERO;
  128. StakingWalletVo vo = new StakingWalletVo();
  129. vo.setCoinUnit(coinUnit.toUpperCase());
  130. vo.setLockedBalance(locked);
  131. vo.setLockedBalanceUsdt(lockedUsdt);
  132. vo.setReleasedTotal(released);
  133. return vo;
  134. }
  135. /**
  136. * 用户发起质押,从资金账户冻结指定金额
  137. *
  138. * @param member 当前登录用户
  139. * @param configId 质押配置ID
  140. * @param amount 质押金额
  141. * @return 操作结果
  142. */
  143. @ApiOperation(value = "发起质押", notes = "选择质押配置并指定金额进行质押,资金将从可用余额冻结")
  144. @PostMapping("/stake")
  145. public MessageResult stake(@SessionAttribute(SESSION_MEMBER) AuthMember member,
  146. @RequestParam Long configId,
  147. @RequestParam(required = false) BigDecimal amount,
  148. @RequestParam(required = false) BigDecimal usdtAmount) {
  149. MessageResult result;
  150. if (usdtAmount != null && usdtAmount.compareTo(BigDecimal.ZERO) > 0) {
  151. result = stakingService.stakeWithUsdt(member.getId(), configId, usdtAmount);
  152. } else {
  153. if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
  154. return MessageResult.error(msService.getMessage("STAKING_AMOUNT_INVALID"));
  155. }
  156. result = stakingService.stake(member.getId(), configId, amount);
  157. }
  158. // 事务提交后发送 MQ 通知,避免消费方读到事务未提交的旧数据
  159. if (result.getCode() == 0) {
  160. kafkaTemplate.send("trading-account-change", member.getId() + "");
  161. }
  162. return result;
  163. }
  164. /**
  165. * 分页查询当前用户的质押订单列表
  166. *
  167. * @param member 当前登录用户
  168. * @param pageNo 页码(从1开始)
  169. * @param pageSize 每页条数
  170. * @param status 状态,支持单个值或逗号分隔,如 0 或 0,1
  171. * @return 质押订单分页数据
  172. */
  173. @ApiOperation(value = "查询我的质押订单", notes = "分页查询当前用户的质押订单,status 支持单个值或逗号分隔,如 0,1")
  174. @PostMapping("/orders")
  175. public MessageResult getMyOrders(@SessionAttribute(SESSION_MEMBER) AuthMember member,
  176. @RequestParam(defaultValue = "1") int pageNo,
  177. @RequestParam(defaultValue = "10") int pageSize,
  178. @RequestParam(required = false) String status) {
  179. List<Integer> statuses;
  180. try {
  181. statuses = parseStatusList(status);
  182. } catch (NumberFormatException e) {
  183. return MessageResult.error(msService.getMessage("PARAMETER_ERROR"));
  184. }
  185. Page<StakingOrder> page = stakingService.getUserStakingOrders(member.getId(), pageNo, pageSize, statuses);
  186. return MessageResult.success("success", page);
  187. }
  188. private List<Integer> parseStatusList(String status) {
  189. if (status == null || status.trim().isEmpty()) {
  190. return null;
  191. }
  192. List<Integer> statuses = new ArrayList<>();
  193. String[] parts = status.split(",");
  194. for (String part : parts) {
  195. String value = part.trim();
  196. if (!value.isEmpty()) {
  197. statuses.add(Integer.valueOf(value));
  198. }
  199. }
  200. return statuses.isEmpty() ? null : statuses;
  201. }
  202. /**
  203. * 分页查询当前用户的质押和释放流水记录
  204. *
  205. * @param member 当前登录用户
  206. * @param pageNo 页码(从1开始)
  207. * @param pageSize 每页条数
  208. * @return 质押流水分页数据
  209. */
  210. @ApiOperation(value = "查询我的质押流水", notes = "分页查询当前用户的质押和解冻释放流水记录")
  211. @PostMapping("/records")
  212. public MessageResult getMyRecords(@SessionAttribute(SESSION_MEMBER) AuthMember member,
  213. @RequestParam(defaultValue = "1") int pageNo,
  214. @RequestParam(defaultValue = "10") int pageSize) {
  215. Page<StakingRecord> page = stakingService.getUserStakingRecords(member.getId(), pageNo, pageSize);
  216. return MessageResult.success("success", page);
  217. }
  218. }