StakingController.java 9.9 KB

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