This commit is contained in:
sion
2026-04-04 21:19:29 +08:00
parent 2850629e0d
commit 37290e7846
166 changed files with 1303 additions and 220663 deletions

View File

@@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 福利接口
* 福利中心接口
*/
@RestController
@RequestMapping("/api/bonus")
@@ -19,28 +19,52 @@ public class BonusController {
private BonusService bonusService;
/**
* 查询是否已领取新人福利
* 获取福利中心状态
*/
@GetMapping("/status")
public Result<Map<String, Object>> getStatus() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
return Result.success(bonusService.getStatus(userId));
}
/**
* 领取新人福利
*/
@PostMapping("/claim")
public Result<Map<String, Object>> claim() {
@GetMapping("/welfare")
public Result<Map<String, Object>> getWelfareStatus() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
return Result.success(bonusService.claim(userId));
return Result.success(bonusService.getWelfareStatus(userId));
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 领取奖励
* body: { type: "new_user"|"referral", referredUserId?, milestone? }
*/
@PostMapping("/claim")
public Result<Map<String, Object>> claim(@RequestBody Map<String, Object> params) {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
String type = (String) params.get("type");
if (type == null || type.isEmpty()) {
return Result.fail("请指定奖励类型");
}
try {
if ("new_user".equals(type)) {
return Result.success("领取成功", bonusService.claimNewUserBonus(userId));
} else if ("referral".equals(type)) {
Object referredUserIdObj = params.get("referredUserId");
Object milestoneObj = params.get("milestone");
if (referredUserIdObj == null || milestoneObj == null) {
return Result.fail("缺少参数");
}
Long referredUserId = Long.valueOf(referredUserIdObj.toString());
int milestone = Integer.parseInt(milestoneObj.toString());
return Result.success("领取成功", bonusService.claimReferralBonus(userId, referredUserId, milestone));
} else {
return Result.fail("无效的奖励类型");
}
} catch (Exception e) {
return Result.fail(e.getMessage());
}

View File

@@ -40,8 +40,8 @@ public class FundController {
BigDecimal amount = request.getAmount();
String remark = request.getRemark();
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
return Result.fail("充值金额必须大于0");
if (amount == null || amount.compareTo(new BigDecimal("1000")) < 0) {
return Result.fail("单笔充值金额不能低于1000 USDT");
}
try {
@@ -85,12 +85,6 @@ public class FundController {
return Result.unauthorized("请先登录");
}
// KYC校验提现前必须完成实名认证
User user = userService.getById(userId);
if (user == null || user.getKycStatus() == null || user.getKycStatus() != 2) {
return Result.fail("KYC_REQUIRED", "请先完成实名认证");
}
BigDecimal amount = request.getAmount();
String withdrawAddress = request.getWithdrawAddress();
String withdrawContact = request.getWithdrawContact();

View File

@@ -19,12 +19,15 @@ public class UserController {
private UserService userService;
/**
* 用户注册
* 用户注册multipart含身份证图片和可选推广码
*/
@PostMapping("/register")
public Result<Map<String, Object>> register(@RequestBody Map<String, String> params) {
String username = params.get("username");
String password = params.get("password");
public Result<Map<String, Object>> register(
@RequestPart("username") String username,
@RequestPart("password") String password,
@RequestPart(value = "referralCode", required = false) String referralCode,
@RequestPart("front") MultipartFile front,
@RequestPart("back") MultipartFile back) {
if (username == null || username.trim().isEmpty()) {
return Result.fail("用户名不能为空");
@@ -34,7 +37,8 @@ public class UserController {
}
try {
Map<String, Object> result = userService.register(username.trim(), password);
Map<String, Object> result = userService.register(
username.trim(), password, referralCode, front, back);
return Result.success("注册成功", result);
} catch (Exception e) {
return Result.fail(e.getMessage());
@@ -109,12 +113,29 @@ public class UserController {
}
}
/**
* 获取推广码
*/
@GetMapping("/referral-code")
public Result<String> getReferralCode() {
Long userId = UserContext.getUserId();
if (userId == null) {
return Result.unauthorized("请先登录");
}
try {
String code = userService.getReferralCode(userId);
return Result.success(code);
} catch (Exception e) {
return Result.fail(e.getMessage());
}
}
/**
* 退出登录
*/
@PostMapping("/logout")
public Result<Void> logout() {
// 客户端清除Token即可
return Result.success("退出成功", null);
}
}

View File

@@ -49,6 +49,12 @@ public class User implements Serializable {
/** 身份证反面照URL */
private String idCardBack;
/** 推广码 */
private String referralCode;
/** 推广人用户ID */
private Long referredBy;
/** 状态: 0-禁用 1-正常 */
private Integer status;

View File

@@ -4,25 +4,35 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.it.rattan.monisuo.entity.AccountFlow;
import com.it.rattan.monisuo.entity.AccountFund;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.AccountFlowMapper;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
import com.it.rattan.monisuo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
/**
* 福利服务
* 福利中心服务
*
* 规则:
* 1. 首充福利新用户首次充值完成后可领取100u一次性
* 2. 推广奖励被推广人累计充值每满1000u推广人领100u多次触发最高8次/人)
*/
@Service
public class BonusService {
private static final BigDecimal BONUS_AMOUNT = new BigDecimal("50");
private static final String BONUS_REMARK = "新人福利";
private static final BigDecimal BONUS_AMOUNT = new BigDecimal("100");
private static final BigDecimal MILESTONE_UNIT = new BigDecimal("1000");
private static final int MAX_MILESTONES = 8;
private static final String NEW_USER_REMARK = "新人首充福利";
private static final String REFERRAL_REMARK_PREFIX = "邀请奖励-";
@Autowired
private AccountFundMapper accountFundMapper;
@@ -33,52 +43,113 @@ public class BonusService {
@Autowired
private AssetService assetService;
@Autowired
private UserMapper userMapper;
/**
* 查询领取状态
* 获取福利中心状态
*/
public Map<String, Object> getStatus(Long userId) {
public Map<String, Object> getWelfareStatus(Long userId) {
Map<String, Object> result = new HashMap<>();
result.put("amount", BONUS_AMOUNT);
result.put("claimed", hasClaimed(userId));
// 用户推广码
User user = userMapper.selectById(userId);
result.put("referralCode", user != null ? user.getReferralCode() : null);
// 首充福利状态
AccountFund fund = assetService.getOrCreateFundAccount(userId);
BigDecimal totalDeposit = fund.getTotalDeposit() != null ? fund.getTotalDeposit() : BigDecimal.ZERO;
boolean hasDeposited = totalDeposit.compareTo(BigDecimal.ZERO) > 0;
boolean newUserClaimed = hasClaimedByRemark(userId, NEW_USER_REMARK);
Map<String, Object> newUserBonus = new HashMap<>();
newUserBonus.put("amount", BONUS_AMOUNT);
newUserBonus.put("eligible", hasDeposited && !newUserClaimed);
newUserBonus.put("claimed", newUserClaimed);
newUserBonus.put("deposited", hasDeposited);
result.put("newUserBonus", newUserBonus);
// 推广奖励状态
List<Map<String, Object>> referralRewards = new ArrayList<>();
// 查找该用户推广的所有人
LambdaQueryWrapper<User> referredWrapper = new LambdaQueryWrapper<>();
referredWrapper.eq(User::getReferredBy, userId);
List<User> referredUsers = userMapper.selectList(referredWrapper);
for (User referred : referredUsers) {
Map<String, Object> referralInfo = new HashMap<>();
referralInfo.put("userId", referred.getId());
referralInfo.put("username", referred.getUsername());
// 被推广人的累计充值
AccountFund referredFund = assetService.getOrCreateFundAccount(referred.getId());
BigDecimal referredTotalDeposit = referredFund.getTotalDeposit() != null
? referredFund.getTotalDeposit() : BigDecimal.ZERO;
referralInfo.put("totalDeposit", referredTotalDeposit);
// 计算里程碑数
int earnedMilestones = referredTotalDeposit.divide(MILESTONE_UNIT, 0, RoundingMode.DOWN).intValue();
earnedMilestones = Math.min(earnedMilestones, MAX_MILESTONES);
// 查询已领取的里程碑
Set<Integer> claimedMilestones = getClaimedMilestones(userId, referred.getId());
List<Map<String, Object>> milestones = new ArrayList<>();
for (int i = 1; i <= MAX_MILESTONES; i++) {
Map<String, Object> m = new HashMap<>();
m.put("milestone", i);
m.put("amount", BONUS_AMOUNT);
m.put("threshold", MILESTONE_UNIT.multiply(new BigDecimal(i)));
m.put("earned", i <= earnedMilestones);
m.put("claimed", claimedMilestones.contains(i));
m.put("claimable", i <= earnedMilestones && !claimedMilestones.contains(i));
milestones.add(m);
}
referralInfo.put("milestones", milestones);
// 可领取数量
int claimable = (int) milestones.stream()
.filter(m -> Boolean.TRUE.equals(m.get("claimable")))
.count();
referralInfo.put("claimableCount", claimable);
referralRewards.add(referralInfo);
}
result.put("referralRewards", referralRewards);
// 总可领取数量
int totalClaimable = (Boolean.TRUE.equals(newUserBonus.get("eligible")) ? 1 : 0);
totalClaimable += referralRewards.stream()
.mapToInt(r -> r.get("claimableCount") instanceof Number
? ((Number) r.get("claimableCount")).intValue() : 0)
.sum();
result.put("totalClaimable", totalClaimable);
return result;
}
/**
* 领取新人福利
* 领取首充福利
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> claim(Long userId) {
// 检查是否已领取
if (hasClaimed(userId)) {
throw new RuntimeException("您已领取过新人福利");
public Map<String, Object> claimNewUserBonus(Long userId) {
if (hasClaimedByRemark(userId, NEW_USER_REMARK)) {
throw new RuntimeException("您已领取过首充福利");
}
// 获取资金账户
AccountFund fund = assetService.getOrCreateFundAccount(userId);
BigDecimal totalDeposit = fund.getTotalDeposit() != null ? fund.getTotalDeposit() : BigDecimal.ZERO;
if (totalDeposit.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("尚未完成充值,无法领取");
}
BigDecimal balanceBefore = fund.getBalance();
BigDecimal balanceAfter = balanceBefore.add(BONUS_AMOUNT);
// 更新余额
fund.setBalance(balanceAfter);
fund.setTotalDeposit(fund.getTotalDeposit().add(BONUS_AMOUNT));
accountFundMapper.update(null, new LambdaUpdateWrapper<AccountFund>()
.eq(AccountFund::getId, fund.getId())
.set(AccountFund::getBalance, balanceAfter)
.set(AccountFund::getTotalDeposit, fund.getTotalDeposit())
.set(AccountFund::getUpdateTime, LocalDateTime.now()));
// 记录流水
AccountFlow flow = new AccountFlow();
flow.setUserId(userId);
flow.setFlowNo(com.it.rattan.monisuo.util.OrderNoUtil.flowNo());
flow.setFlowType(1); // 充值类型
flow.setAmount(BONUS_AMOUNT);
flow.setBalanceBefore(balanceBefore);
flow.setBalanceAfter(balanceAfter);
flow.setCoinCode("USDT");
flow.setRemark(BONUS_REMARK);
flow.setCreateTime(LocalDateTime.now());
accountFlowMapper.insert(flow);
updateFundBalance(fund, balanceAfter);
assetService.createFlow(userId, 1, BONUS_AMOUNT, balanceBefore, balanceAfter,
"USDT", null, NEW_USER_REMARK);
Map<String, Object> result = new HashMap<>();
result.put("amount", BONUS_AMOUNT);
@@ -87,12 +158,96 @@ public class BonusService {
}
/**
* 检查是否已领取(通过流水记录判断)
* 领取推广奖励
*/
private boolean hasClaimed(Long userId) {
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> claimReferralBonus(Long referrerId, Long referredUserId, int milestone) {
if (milestone < 1 || milestone > MAX_MILESTONES) {
throw new RuntimeException("无效的里程碑");
}
// 验证推广关系
User referred = userMapper.selectById(referredUserId);
if (referred == null || !referrerId.equals(referred.getReferredBy())) {
throw new RuntimeException("推广关系不存在");
}
// 检查里程碑是否已达到
AccountFund referredFund = assetService.getOrCreateFundAccount(referredUserId);
BigDecimal referredTotalDeposit = referredFund.getTotalDeposit() != null
? referredFund.getTotalDeposit() : BigDecimal.ZERO;
int earnedMilestones = referredTotalDeposit
.divide(MILESTONE_UNIT, 0, RoundingMode.DOWN).intValue();
if (milestone > earnedMilestones) {
throw new RuntimeException("该里程碑尚未达到");
}
// 检查是否已领取
String remark = REFERRAL_REMARK_PREFIX + referredUserId + "-" + milestone;
if (hasClaimedByRemark(referrerId, remark)) {
throw new RuntimeException("该里程碑奖励已领取");
}
// 发放奖励
AccountFund referrerFund = assetService.getOrCreateFundAccount(referrerId);
BigDecimal balanceBefore = referrerFund.getBalance();
BigDecimal balanceAfter = balanceBefore.add(BONUS_AMOUNT);
updateFundBalance(referrerFund, balanceAfter);
assetService.createFlow(referrerId, 1, BONUS_AMOUNT, balanceBefore, balanceAfter,
"USDT", null, remark);
Map<String, Object> result = new HashMap<>();
result.put("amount", BONUS_AMOUNT);
result.put("balance", balanceAfter);
result.put("referredUserId", referredUserId);
result.put("milestone", milestone);
return result;
}
/**
* 更新资金账户余额
*/
private void updateFundBalance(AccountFund fund, BigDecimal newBalance) {
LocalDateTime now = LocalDateTime.now();
LambdaUpdateWrapper<AccountFund> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(AccountFund::getId, fund.getId())
.set(AccountFund::getBalance, newBalance)
.set(AccountFund::getUpdateTime, now);
accountFundMapper.update(null, wrapper);
}
/**
* 检查是否已通过 remark 领取过
*/
private boolean hasClaimedByRemark(Long userId, String remark) {
LambdaQueryWrapper<AccountFlow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountFlow::getUserId, userId)
.eq(AccountFlow::getRemark, BONUS_REMARK);
.eq(AccountFlow::getRemark, remark);
return accountFlowMapper.selectCount(wrapper) > 0;
}
/**
* 获取已领取的推广里程碑集合
*/
private Set<Integer> getClaimedMilestones(Long referrerId, Long referredUserId) {
String prefix = REFERRAL_REMARK_PREFIX + referredUserId + "-";
LambdaQueryWrapper<AccountFlow> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AccountFlow::getUserId, referrerId)
.likeRight(AccountFlow::getRemark, prefix);
List<AccountFlow> flows = accountFlowMapper.selectList(wrapper);
Set<Integer> milestones = new HashSet<>();
for (AccountFlow flow : flows) {
String remark = flow.getRemark();
if (remark != null && remark.startsWith(prefix)) {
try {
int m = Integer.parseInt(remark.substring(prefix.length()));
milestones.add(m);
} catch (NumberFormatException ignored) {
}
}
}
return milestones;
}
}

View File

@@ -53,8 +53,18 @@ public class FundService {
*/
@Transactional
public Map<String, Object> deposit(Long userId, BigDecimal amount, String remark) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("充值金额必须大于0");
if (amount.compareTo(new BigDecimal("1000")) < 0) {
throw new RuntimeException("单笔充值金额不能低于1000 USDT");
}
// 检查累计充值上限
AccountFund fund = accountFundMapper.selectOne(
new LambdaQueryWrapper<AccountFund>().eq(AccountFund::getUserId, userId));
if (fund != null) {
BigDecimal totalAfterDeposit = fund.getTotalDeposit().add(amount);
if (totalAfterDeposit.compareTo(new BigDecimal("8000")) > 0) {
throw new RuntimeException("累计充值金额不能超过8000 USDT");
}
}
User user = userMapper.selectById(userId);

View File

@@ -2,7 +2,6 @@ package com.it.rattan.monisuo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.it.rattan.monisuo.context.UserContext;
import com.it.rattan.monisuo.entity.AccountFund;
import com.it.rattan.monisuo.entity.User;
import com.it.rattan.monisuo.mapper.AccountFundMapper;
@@ -18,6 +17,7 @@ import java.io.IOException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
/**
* 用户服务
@@ -32,12 +32,14 @@ public class UserService extends ServiceImpl<UserMapper, User> {
private AccountFundMapper accountFundMapper;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private static final String CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
/**
* 用户注册
* 用户注册含KYC和推广码
*/
@Transactional
public Map<String, Object> register(String username, String password) {
public Map<String, Object> register(String username, String password, String referralCode,
MultipartFile frontFile, MultipartFile backFile) throws IOException {
// 检查用户名是否存在
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
@@ -53,6 +55,40 @@ public class UserService extends ServiceImpl<UserMapper, User> {
user.setKycStatus(0);
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
// 生成唯一推广码
user.setReferralCode(generateReferralCode());
// 处理推广码:如果填了推广码,查找推广人
if (referralCode != null && !referralCode.trim().isEmpty()) {
LambdaQueryWrapper<User> refWrapper = new LambdaQueryWrapper<>();
refWrapper.eq(User::getReferralCode, referralCode.trim());
User referrer = userMapper.selectOne(refWrapper);
if (referrer == null) {
throw new RuntimeException("推广码不存在");
}
user.setReferredBy(referrer.getId());
}
// 保存身份证图片
if (frontFile != null && !frontFile.isEmpty() && backFile != null && !backFile.isEmpty()) {
try {
String uploadDir = System.getProperty("user.dir") + File.separator + "uploads" + File.separator + "kyc";
File dir = new File(uploadDir);
if (!dir.exists()) {
dir.mkdirs();
}
String timestamp = String.valueOf(System.currentTimeMillis());
String frontName = saveFile(frontFile, dir, "reg_" + timestamp + "_front");
String backName = saveFile(backFile, dir, "reg_" + timestamp + "_back");
user.setIdCardFront("/uploads/kyc/" + frontName);
user.setIdCardBack("/uploads/kyc/" + backName);
user.setKycStatus(2); // 虚拟KYC提交即自动通过
} catch (IOException e) {
throw new RuntimeException("身份证图片保存失败: " + e.getMessage());
}
}
userMapper.insert(user);
// 初始化资金账户
@@ -67,8 +103,6 @@ public class UserService extends ServiceImpl<UserMapper, User> {
// 生成Token
String token = JwtUtil.createToken(user.getId(), username, "user");
// 更新Token
user.setToken(token);
userMapper.updateById(user);
@@ -82,7 +116,6 @@ public class UserService extends ServiceImpl<UserMapper, User> {
* 用户登录
*/
public Map<String, Object> login(String username, String password) {
// 查询用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userMapper.selectOne(wrapper);
@@ -90,19 +123,14 @@ public class UserService extends ServiceImpl<UserMapper, User> {
if (user == null) {
throw new RuntimeException("用户不存在");
}
if (user.getStatus() == 0) {
throw new RuntimeException("账号已被禁用");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("密码错误");
}
// 生成新Token
String token = JwtUtil.createToken(user.getId(), username, "user");
// 更新登录信息
user.setToken(token);
user.setLastLoginTime(LocalDateTime.now());
userMapper.updateById(user);
@@ -135,7 +163,6 @@ public class UserService extends ServiceImpl<UserMapper, User> {
}
try {
// 上传目录:项目工作目录下的 uploads/kyc/
String uploadDir = System.getProperty("user.dir") + File.separator + "uploads" + File.separator + "kyc";
File dir = new File(uploadDir);
if (!dir.exists()) {
@@ -146,10 +173,9 @@ public class UserService extends ServiceImpl<UserMapper, User> {
String frontPath = saveFile(frontFile, dir, userId + "_front_" + timestamp);
String backPath = saveFile(backFile, dir, userId + "_back_" + timestamp);
// 存储相对访问路径
user.setIdCardFront("/uploads/kyc/" + frontPath);
user.setIdCardBack("/uploads/kyc/" + backPath);
user.setKycStatus(2); // 虚拟KYC提交即自动通过
user.setKycStatus(2);
user.setUpdateTime(LocalDateTime.now());
userMapper.updateById(user);
} catch (IOException e) {
@@ -157,6 +183,37 @@ public class UserService extends ServiceImpl<UserMapper, User> {
}
}
/**
* 获取用户推广码
*/
public String getReferralCode(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return user.getReferralCode();
}
/**
* 生成唯一推广码
*/
private String generateReferralCode() {
Random random = new Random();
for (int i = 0; i < 100; i++) {
StringBuilder sb = new StringBuilder(8);
for (int j = 0; j < 8; j++) {
sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
}
String code = sb.toString();
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getReferralCode, code);
if (userMapper.selectCount(wrapper) == 0) {
return code;
}
}
throw new RuntimeException("生成推广码失败,请重试");
}
/**
* 保存上传文件,返回保存的文件名
*/
@@ -186,6 +243,7 @@ public class UserService extends ServiceImpl<UserMapper, User> {
info.put("email", user.getEmail());
info.put("kycStatus", user.getKycStatus());
info.put("status", user.getStatus());
info.put("referralCode", user.getReferralCode());
return info;
}
}