first commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.promo.config;
|
||||
|
||||
import com.ruoyi.promo.interceptor.PromoMaintenanceInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 推广后台 Web 配置:注册维护拦截器。
|
||||
* <p>
|
||||
* 对 /promo/** 生效,放行登录/注册接口(登录/注册由 PromoLoginController 自行提示维护)。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
public class PromoWebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private PromoMaintenanceInterceptor promoMaintenanceInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(promoMaintenanceInterceptor)
|
||||
.addPathPatterns("/promo/**")
|
||||
.excludePathPatterns("/promo/login", "/promo/smsLogin", "/promo/register");
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.ruoyi.promo.contract.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GetPurchaseByUserIdRequest {
|
||||
/**
|
||||
* 玩家ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
private String beginTime;
|
||||
|
||||
private String endTime;
|
||||
private Integer pageNum;
|
||||
private Integer pageSize;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.promo.contract.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GetSubBranchesRequest {
|
||||
/**
|
||||
* 主播ID
|
||||
*/
|
||||
private Integer userId;
|
||||
/**
|
||||
* 玩家id
|
||||
*/
|
||||
private Integer playerId;
|
||||
|
||||
private Integer pageNum;
|
||||
private Integer pageSize;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.promo.contract.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class QueryPromoteWelfareRequest {
|
||||
private Integer pageNum;
|
||||
private Integer pageSize;
|
||||
private String beginTime;
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.promo.contract.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
@NotNull(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotNull(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
private String contactInformation;
|
||||
|
||||
private String parentInvitationCode;
|
||||
|
||||
@NotNull(message = "注册角色不能为空")
|
||||
private String userType;
|
||||
|
||||
private String code;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.promo.contract.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UpdateInvitationCodeRequest {
|
||||
@NotNull(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotNull(message = "邀请码不能为空")
|
||||
private String invitationCode;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.ruoyi.promo.contract.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GetShareDomainResponse {
|
||||
private String zhuboShareDomain;
|
||||
private String zhaoshaoShareDomain;
|
||||
private String userShareDomain;
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.promo.contract.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GetSubBranchesResponse {
|
||||
private Integer total;
|
||||
|
||||
private List<UserInfo> rows;
|
||||
|
||||
@Data
|
||||
public static class UserInfo {
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String nickName;
|
||||
//01主播 02普通用户
|
||||
private String userType;
|
||||
/**
|
||||
* 佣金比例
|
||||
*/
|
||||
private BigDecimal commissionRate;
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
|
||||
private BigDecimal totalRecharge;
|
||||
private BigDecimal totalCost;
|
||||
private BigDecimal totalNormGameCost;
|
||||
private BigDecimal totalSpecialGameCost;
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.promo.contract.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class QueryPromotionWelfareResponse {
|
||||
private Integer total;
|
||||
private List<Record> rows;
|
||||
private BigDecimal totalReward;
|
||||
|
||||
@Data
|
||||
public static class Record {
|
||||
private BigDecimal reward;
|
||||
private String date;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.promo.contract.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class DailyUserErcashVO {
|
||||
private String date;
|
||||
private BigDecimal dailyNormGameCost;
|
||||
private BigDecimal dailySpecialGameCost;
|
||||
private BigDecimal dailyRecharge;
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
import com.ruoyi.admin.mapper.PromoAdminMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordType;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.domain.entity.sys.PromoAdmin;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.domain.entity.PromoCardKey;
|
||||
import com.ruoyi.promo.mapper.PromoCardKeyMapper;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 超级管理员专属管理接口
|
||||
*/
|
||||
@Api(tags = "超管管理")
|
||||
@RestController
|
||||
@RequestMapping("/promo/adminManage")
|
||||
public class PromoAdminManageController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper ttUserMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
|
||||
|
||||
@Autowired
|
||||
private PromoCardKeyMapper promoCardKeyMapper;
|
||||
|
||||
@Autowired
|
||||
private PromoAdminMapper promoAdminMapper;
|
||||
|
||||
/**
|
||||
* 检查是否为超级管理员
|
||||
*/
|
||||
private boolean isSuperAdmin() {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return false;
|
||||
}
|
||||
return UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 招商管理:查看所有招商及其下属主播汇总数据
|
||||
*/
|
||||
@ApiOperation("招商管理列表")
|
||||
@GetMapping("/businessList")
|
||||
public R<List<Map<String, Object>>> getBusinessList() {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
|
||||
// 查询所有招商用户(排除白号:白号数据不在招商管理体现)
|
||||
LambdaQueryWrapper<TtUser> wrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.eq(TtUser::getUserType, UserType.BUSINESS_USER.getCode())
|
||||
.and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1));
|
||||
List<TtUser> businessUsers = ttUserMapper.selectList(wrapper);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (TtUser business : businessUsers) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("userId", business.getUserId());
|
||||
item.put("userName", business.getUserName());
|
||||
item.put("nickName", business.getNickName());
|
||||
item.put("createTime", business.getCreateTime());
|
||||
|
||||
// 查询下属主播(排除白号)
|
||||
LambdaQueryWrapper<TtUser> anchorWrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.eq(TtUser::getParentId, business.getUserId())
|
||||
.and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1));
|
||||
List<TtUser> anchors = ttUserMapper.selectList(anchorWrapper);
|
||||
|
||||
item.put("anchorCount", anchors.size());
|
||||
|
||||
// 汇总下属主播的下属用户数据
|
||||
BigDecimal totalRecharge = BigDecimal.ZERO;
|
||||
BigDecimal totalTurnover = BigDecimal.ZERO;
|
||||
BigDecimal totalSpecialTurnover = BigDecimal.ZERO;
|
||||
BigDecimal totalClassicTurnover = BigDecimal.ZERO;
|
||||
BigDecimal totalEarnings = BigDecimal.ZERO; // 招商总收益
|
||||
|
||||
List<Map<String, Object>> anchorDetails = new ArrayList<>();
|
||||
for (TtUser anchor : anchors) {
|
||||
// 获取主播下属的所有普通用户(排除白号,白号充值/流水不计入统计)
|
||||
List<Integer> subUserIds = filterOutBlankAccounts(ttUserMapper.allEmployeesByParents(
|
||||
Collections.singletonList(anchor.getUserId())));
|
||||
|
||||
Map<String, Object> anchorItem = new LinkedHashMap<>();
|
||||
anchorItem.put("userId", anchor.getUserId());
|
||||
anchorItem.put("userName", anchor.getUserName());
|
||||
anchorItem.put("nickName", anchor.getNickName());
|
||||
anchorItem.put("userCount", subUserIds != null ? subUserIds.size() : 0);
|
||||
|
||||
BigDecimal anchorRecharge = BigDecimal.ZERO;
|
||||
BigDecimal anchorTurnover = BigDecimal.ZERO;
|
||||
BigDecimal anchorSpecialTurnover = BigDecimal.ZERO; // 特殊游戏(玫瑰女郎)流水
|
||||
BigDecimal anchorClassicTurnover = BigDecimal.ZERO; // 经典游戏流水
|
||||
BigDecimal anchorEarnings = BigDecimal.ZERO;
|
||||
BigDecimal businessEarningsForAnchor = BigDecimal.ZERO; // 招商从该主播获得的收益
|
||||
// 主播本人充值(无论有没有下级都需要查)
|
||||
BigDecimal anchorSelfRecharge = sumBlendErcash(
|
||||
Collections.singletonList(anchor.getUserId()),
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode());
|
||||
|
||||
if (CollectionUtils.isNotEmpty(subUserIds)) {
|
||||
// 总充值
|
||||
anchorRecharge = sumBlendErcash(subUserIds,
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode());
|
||||
// 总流水(游戏消费)
|
||||
anchorTurnover = sumGameConsume(subUserIds);
|
||||
// 特殊游戏(玫瑰女郎)消费
|
||||
anchorSpecialTurnover = sumNewGameConsume(subUserIds);
|
||||
// 经典游戏消费
|
||||
anchorClassicTurnover = sumClassicGameConsume(subUserIds);
|
||||
BigDecimal newGameConsume = anchorSpecialTurnover;
|
||||
BigDecimal classicGameConsume = anchorClassicTurnover;
|
||||
|
||||
// 主播收益 = 下级充值×主播充值比例 + 下级新游戏消费×主播新游戏比例 + 下级经典游戏消费×主播经典比例
|
||||
anchorEarnings = calcRate(anchorRecharge, anchor.getCommissionRate())
|
||||
.add(calcRate(newGameConsume, anchor.getNewGameCommissionRate()))
|
||||
.add(calcRate(classicGameConsume, anchor.getClassicGameCommissionRate()));
|
||||
|
||||
// 招商收益 = 主播本人充值×招商充值比例
|
||||
// + 主播下级充值×招商充值比例
|
||||
// + 主播下级新游戏消费×招商新游戏比例
|
||||
// + 主播下级经典游戏消费×招商经典比例
|
||||
businessEarningsForAnchor = calcRate(anchorSelfRecharge, business.getCommissionRate())
|
||||
.add(calcRate(anchorRecharge, business.getCommissionRate()))
|
||||
.add(calcRate(newGameConsume, business.getNewGameCommissionRate()))
|
||||
.add(calcRate(classicGameConsume, business.getClassicGameCommissionRate()));
|
||||
|
||||
// 每个普通用户详细数据
|
||||
LambdaQueryWrapper<TtUser> userWrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.in(TtUser::getUserId, subUserIds)
|
||||
.orderByDesc(TtUser::getCreateTime);
|
||||
List<TtUser> subUsers = ttUserMapper.selectList(userWrapper);
|
||||
List<Map<String, Object>> userDetails = new ArrayList<>();
|
||||
for (TtUser user : subUsers) {
|
||||
Map<String, Object> userItem = new LinkedHashMap<>();
|
||||
userItem.put("userId", user.getUserId());
|
||||
userItem.put("userName", user.getUserName());
|
||||
userItem.put("nickName", user.getNickName());
|
||||
userItem.put("createTime", user.getCreateTime());
|
||||
List<Integer> singleList = Collections.singletonList(user.getUserId());
|
||||
userItem.put("totalRecharge", sumBlendErcash(singleList,
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode()));
|
||||
userItem.put("totalTurnover", sumGameConsume(singleList));
|
||||
userItem.put("specialGameTurnover", sumNewGameConsume(singleList));
|
||||
userItem.put("classicGameTurnover", sumClassicGameConsume(singleList));
|
||||
userDetails.add(userItem);
|
||||
}
|
||||
anchorItem.put("users", userDetails);
|
||||
}
|
||||
|
||||
anchorItem.put("totalRecharge", anchorRecharge);
|
||||
BigDecimal anchorSelf = anchorSelfRecharge != null ? anchorSelfRecharge : BigDecimal.ZERO;
|
||||
anchorItem.put("selfRecharge", anchorSelf);
|
||||
anchorItem.put("totalTurnover", anchorTurnover);
|
||||
anchorItem.put("specialGameTurnover", anchorSpecialTurnover);
|
||||
anchorItem.put("classicGameTurnover", anchorClassicTurnover);
|
||||
anchorItem.put("totalEarnings", anchorEarnings);
|
||||
anchorDetails.add(anchorItem);
|
||||
|
||||
// 招商总充值 = 主播本人充值 + 主播下级充值
|
||||
totalRecharge = totalRecharge.add(anchorRecharge).add(anchorSelf);
|
||||
totalTurnover = totalTurnover.add(anchorTurnover);
|
||||
totalSpecialTurnover = totalSpecialTurnover.add(anchorSpecialTurnover);
|
||||
totalClassicTurnover = totalClassicTurnover.add(anchorClassicTurnover);
|
||||
totalEarnings = totalEarnings.add(businessEarningsForAnchor);
|
||||
}
|
||||
|
||||
item.put("totalRecharge", totalRecharge);
|
||||
item.put("totalTurnover", totalTurnover);
|
||||
item.put("specialGameTurnover", totalSpecialTurnover);
|
||||
item.put("classicGameTurnover", totalClassicTurnover);
|
||||
item.put("totalEarnings", totalEarnings);
|
||||
item.put("anchors", anchorDetails);
|
||||
result.add(item);
|
||||
}
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主播管理:查看所有主播及其下属用户汇总数据
|
||||
*/
|
||||
@ApiOperation("主播管理列表")
|
||||
@GetMapping("/anchorList")
|
||||
public R<List<Map<String, Object>>> getAnchorList() {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
|
||||
// 查询所有主播用户(排除白号:白号数据不在主播管理体现)
|
||||
LambdaQueryWrapper<TtUser> wrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.eq(TtUser::getUserType, UserType.ANCHOR.getCode())
|
||||
.and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1));
|
||||
List<TtUser> anchors = ttUserMapper.selectList(wrapper);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (TtUser anchor : anchors) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("userId", anchor.getUserId());
|
||||
item.put("userName", anchor.getUserName());
|
||||
item.put("nickName", anchor.getNickName());
|
||||
item.put("parentId", anchor.getParentId());
|
||||
item.put("createTime", anchor.getCreateTime());
|
||||
|
||||
// 查询下属用户(排除白号,白号充值/流水不计入统计)
|
||||
List<Integer> subUserIds = filterOutBlankAccounts(ttUserMapper.allEmployeesByParents(
|
||||
Collections.singletonList(anchor.getUserId())));
|
||||
|
||||
item.put("userCount", subUserIds != null ? subUserIds.size() : 0);
|
||||
|
||||
BigDecimal totalRecharge = BigDecimal.ZERO;
|
||||
BigDecimal totalTurnover = BigDecimal.ZERO;
|
||||
BigDecimal totalEarnings = BigDecimal.ZERO;
|
||||
// 下级聚合分项,无下属时保持 0,仅主播本人充值返利生效
|
||||
BigDecimal newGameConsume = BigDecimal.ZERO;
|
||||
BigDecimal classicGameConsume = BigDecimal.ZERO;
|
||||
|
||||
// 主播本人充值(用于主播本人充值返利分项,口径与 getBusinessList 完全一致)
|
||||
BigDecimal anchorSelfRecharge = sumBlendErcash(
|
||||
Collections.singletonList(anchor.getUserId()),
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode());
|
||||
|
||||
List<Map<String, Object>> userDetails = new ArrayList<>();
|
||||
if (CollectionUtils.isNotEmpty(subUserIds)) {
|
||||
// 总充值
|
||||
totalRecharge = sumBlendErcash(subUserIds,
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode());
|
||||
// 总流水(游戏消费)
|
||||
totalTurnover = sumGameConsume(subUserIds);
|
||||
// 新游戏消费
|
||||
newGameConsume = sumNewGameConsume(subUserIds);
|
||||
// 经典游戏消费
|
||||
classicGameConsume = sumClassicGameConsume(subUserIds);
|
||||
|
||||
// 每个用户详细
|
||||
LambdaQueryWrapper<TtUser> userWrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.in(TtUser::getUserId, subUserIds);
|
||||
List<TtUser> subUsers = ttUserMapper.selectList(userWrapper);
|
||||
for (TtUser user : subUsers) {
|
||||
Map<String, Object> userItem = new LinkedHashMap<>();
|
||||
userItem.put("userId", user.getUserId());
|
||||
userItem.put("userName", user.getUserName());
|
||||
userItem.put("nickName", user.getNickName());
|
||||
userItem.put("createTime", user.getCreateTime());
|
||||
|
||||
List<Integer> singleList = Collections.singletonList(user.getUserId());
|
||||
userItem.put("totalRecharge", sumBlendErcash(singleList,
|
||||
TtAccountRecordSource.RECHARGE.getCode(),
|
||||
TtAccountRecordType.INPUT.getCode()));
|
||||
userItem.put("totalTurnover", sumGameConsume(singleList));
|
||||
userItem.put("specialGameTurnover", sumNewGameConsume(singleList));
|
||||
userItem.put("classicGameTurnover", sumClassicGameConsume(singleList));
|
||||
userDetails.add(userItem);
|
||||
}
|
||||
}
|
||||
|
||||
// 主播收益 = 主播本人充值×主播充值比例 + 下级充值×主播充值比例
|
||||
// + 下级新游戏消费×主播新游戏比例 + 下级经典游戏消费×主播经典比例
|
||||
// 分项粒度与舍入口径与 getBusinessList 完全一致(含主播本人充值返利)
|
||||
// 无条件计算:无下属时后三项为 0,仅主播本人充值返利生效
|
||||
totalEarnings = calcRate(anchorSelfRecharge, anchor.getCommissionRate())
|
||||
.add(calcRate(totalRecharge, anchor.getCommissionRate()))
|
||||
.add(calcRate(newGameConsume, anchor.getNewGameCommissionRate()))
|
||||
.add(calcRate(classicGameConsume, anchor.getClassicGameCommissionRate()));
|
||||
|
||||
item.put("totalRecharge", totalRecharge);
|
||||
item.put("totalTurnover", totalTurnover);
|
||||
item.put("totalEarnings", totalEarnings);
|
||||
item.put("users", userDetails);
|
||||
result.add(item);
|
||||
}
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡商管理:查看所有卡商及其卡密记录
|
||||
*/
|
||||
@ApiOperation("卡商管理列表")
|
||||
@GetMapping("/cardMerchantList")
|
||||
public R<List<Map<String, Object>>> getCardMerchantList() {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
|
||||
// 查询所有卡商用户(排除已删除的)
|
||||
LambdaQueryWrapper<TtUser> wrapper = Wrappers.<TtUser>lambdaQuery()
|
||||
.eq(TtUser::getUserType, UserType.CARD_MERCHANT.getCode())
|
||||
.eq(TtUser::getDelFlag, "0");
|
||||
List<TtUser> merchants = ttUserMapper.selectList(wrapper);
|
||||
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
for (TtUser merchant : merchants) {
|
||||
Map<String, Object> item = new LinkedHashMap<>();
|
||||
item.put("userId", merchant.getUserId());
|
||||
item.put("userName", merchant.getUserName());
|
||||
item.put("nickName", merchant.getNickName());
|
||||
item.put("merchantBalance", merchant.getMerchantBalance() != null ? merchant.getMerchantBalance() : BigDecimal.ZERO);
|
||||
item.put("createTime", merchant.getCreateTime());
|
||||
|
||||
// 查询该卡商的所有卡密
|
||||
LambdaQueryWrapper<PromoCardKey> cardWrapper = Wrappers.<PromoCardKey>lambdaQuery()
|
||||
.eq(PromoCardKey::getCreatorId, merchant.getUserId())
|
||||
.orderByDesc(PromoCardKey::getCreateTime);
|
||||
List<PromoCardKey> cardKeys = promoCardKeyMapper.selectList(cardWrapper);
|
||||
|
||||
item.put("totalCards", cardKeys.size());
|
||||
item.put("usedCards", cardKeys.stream().filter(c -> c.getStatus() == 1).count());
|
||||
item.put("cardKeys", cardKeys);
|
||||
result.add(item);
|
||||
}
|
||||
|
||||
return R.ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤掉白号用户ID:白号(is_blank_account=1)的充值/流水为虚假数据,
|
||||
* 不计入招商/主播管理的金额与收益统计。
|
||||
*/
|
||||
private List<Integer> filterOutBlankAccounts(List<Integer> userIds) {
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return userIds;
|
||||
}
|
||||
return ttUserMapper.selectList(Wrappers.<TtUser>lambdaQuery()
|
||||
.select(TtUser::getUserId)
|
||||
.in(TtUser::getUserId, userIds)
|
||||
.and(w -> w.isNull(TtUser::getIsBlankAccount).or().ne(TtUser::getIsBlankAccount, 1)))
|
||||
.stream().map(TtUser::getUserId).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总指定用户的特定来源流水金额(同时查询主表和归档表)
|
||||
*/
|
||||
private BigDecimal sumBlendErcash(List<Integer> userIds, Integer source, Integer type) {
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(userIds, source, type);
|
||||
return result != null ? result : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总游戏消费流水(总流水,同时查询主表和归档表)
|
||||
*/
|
||||
private BigDecimal sumGameConsume(List<Integer> userIds) {
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
Set<Integer> consumeCodes = TtAccountRecordSource.getGameConsumeCodes();
|
||||
BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSources(
|
||||
userIds, new ArrayList<>(consumeCodes), null);
|
||||
return result != null ? result : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总新游戏消费(奥林匹斯)
|
||||
*/
|
||||
private BigDecimal sumNewGameConsume(List<Integer> userIds) {
|
||||
return sumConsumeByFilter(userIds, TtAccountRecordSource::isNewGameConsumeForCommission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇总经典游戏消费(经典开箱、对战、幸运升级、疯狂转盘)
|
||||
*/
|
||||
private BigDecimal sumClassicGameConsume(List<Integer> userIds) {
|
||||
return sumConsumeByFilter(userIds, TtAccountRecordSource::isClassicGameConsumeForCommission);
|
||||
}
|
||||
|
||||
private BigDecimal sumConsumeByFilter(List<Integer> userIds, java.util.function.IntPredicate filter) {
|
||||
if (CollectionUtils.isEmpty(userIds)) return BigDecimal.ZERO;
|
||||
Set<Integer> consumeCodes = TtAccountRecordSource.getGameConsumeCodes();
|
||||
Set<Integer> filteredCodes = consumeCodes.stream().filter(filter::test).collect(Collectors.toSet());
|
||||
if (filteredCodes.isEmpty()) return BigDecimal.ZERO;
|
||||
BigDecimal result = ttUserBlendErcashMapper.sumAmountByUserIdsAndSources(
|
||||
userIds, new ArrayList<>(filteredCodes), null);
|
||||
return result != null ? result : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按百分比比例计算佣金:amount × rate / 100
|
||||
*/
|
||||
private BigDecimal calcRate(BigDecimal amount, BigDecimal rate) {
|
||||
if (amount == null || rate == null
|
||||
|| amount.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| rate.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return amount.multiply(rate)
|
||||
.divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
// ==================== 推广后台管理员账号管理(仅超管) ====================
|
||||
|
||||
@ApiOperation("管理员列表")
|
||||
@GetMapping("/adminList")
|
||||
public R<List<PromoAdmin>> adminList() {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
List<PromoAdmin> list = promoAdminMapper.selectList(
|
||||
Wrappers.<PromoAdmin>lambdaQuery().orderByAsc(PromoAdmin::getId));
|
||||
list.forEach(a -> a.setPassword(null));
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
@ApiOperation("新增/修改管理员")
|
||||
@PostMapping("/saveAdmin")
|
||||
public R<Void> saveAdmin(@RequestBody PromoAdmin body) {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
String username = StringUtils.trim(body.getUsername());
|
||||
if (StringUtils.isEmpty(username)) {
|
||||
return R.fail("登录账号不能为空");
|
||||
}
|
||||
if (!Validator.isMobile(username)) {
|
||||
return R.fail("登录账号必须是手机号");
|
||||
}
|
||||
if (StringUtils.isEmpty(body.getNickName())) {
|
||||
return R.fail("昵称不能为空");
|
||||
}
|
||||
String newPhone = StringUtils.isEmpty(body.getPhoneNumber())
|
||||
? username : StringUtils.trim(body.getPhoneNumber());
|
||||
PromoAdmin exist = promoAdminMapper.findByUsername(username);
|
||||
|
||||
if (body.getId() == null) {
|
||||
if (StringUtils.isEmpty(body.getPassword())) {
|
||||
return R.fail("密码不能为空");
|
||||
}
|
||||
if (exist != null) {
|
||||
return R.fail("该账号已存在");
|
||||
}
|
||||
// tt_user 里已有该手机号(哪怕现在是普通用户)一律不给加:以后该号变成主播/招商/卡商,
|
||||
// 短信登录会优先命中 tt_user,超管身份就丢了
|
||||
if (existsInTtUser(username) || existsInTtUser(newPhone)) {
|
||||
return R.fail("手机号'" + username + "'在用户表中已存在,不能添加为管理员");
|
||||
}
|
||||
body.setUsername(username);
|
||||
body.setPassword(SecurityUtils.encryptPassword(body.getPassword()));
|
||||
body.setStatus("0");
|
||||
body.setCreateTime(DateUtils.getNowDate());
|
||||
if (StringUtils.isEmpty(body.getPhoneNumber())) {
|
||||
body.setPhoneNumber(username);
|
||||
}
|
||||
promoAdminMapper.insert(body);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
PromoAdmin target = promoAdminMapper.selectById(body.getId());
|
||||
if (target == null) {
|
||||
return R.fail("管理员不存在");
|
||||
}
|
||||
if (exist != null && !exist.getId().equals(target.getId())) {
|
||||
return R.fail("该账号已存在");
|
||||
}
|
||||
// 只在改成新手机号时校验,避免老数据(手机号已在 tt_user)连昵称都改不了
|
||||
if ((!username.equals(target.getUsername()) && existsInTtUser(username))
|
||||
|| (!newPhone.equals(target.getPhoneNumber()) && existsInTtUser(newPhone))) {
|
||||
return R.fail("该手机号在用户表中已存在,不能作为管理员账号");
|
||||
}
|
||||
target.setUsername(username);
|
||||
target.setNickName(body.getNickName());
|
||||
target.setPhoneNumber(newPhone);
|
||||
target.setEmail(body.getEmail());
|
||||
if (StringUtils.isNotEmpty(body.getPassword())) {
|
||||
target.setPassword(SecurityUtils.encryptPassword(body.getPassword()));
|
||||
}
|
||||
promoAdminMapper.updateById(target);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* tt_user 里是否已有该手机号(登录账号或手机号任一命中即算)
|
||||
*/
|
||||
private boolean existsInTtUser(String phone) {
|
||||
if (StringUtils.isEmpty(phone)) {
|
||||
return false;
|
||||
}
|
||||
return new LambdaQueryChainWrapper<>(ttUserMapper)
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.and(w -> w.eq(TtUser::getPhoneNumber, phone).or().eq(TtUser::getUserName, phone))
|
||||
.count() > 0;
|
||||
}
|
||||
|
||||
@ApiOperation("删除管理员")
|
||||
@PostMapping("/deleteAdmin/{id}")
|
||||
public R<Void> deleteAdmin(@PathVariable("id") Integer id) {
|
||||
if (!isSuperAdmin()) {
|
||||
return R.fail("无权限,仅超级管理员可访问");
|
||||
}
|
||||
if (id.equals(getUserId().intValue())) {
|
||||
return R.fail("不能删除当前登录账号");
|
||||
}
|
||||
if (promoAdminMapper.selectCount(null) <= 1) {
|
||||
return R.fail("至少保留一个管理员");
|
||||
}
|
||||
promoAdminMapper.deleteById(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import cloud.tianai.captcha.application.ImageCaptchaApplication;
|
||||
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
|
||||
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
|
||||
import cloud.tianai.captcha.common.response.ApiResponse;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.promo.model.PromoCaptchaCheckRequest;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 推广后台滑块验证码控制器
|
||||
* POST /promo/captcha/gen - 生成验证码
|
||||
* POST /promo/captcha/check - 校验验证码
|
||||
*/
|
||||
@Api(tags = "推广后台滑块验证码")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/promo/captcha")
|
||||
public class PromoCaptchaController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private ImageCaptchaApplication application;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/** 滑块位置容差,0.3 表示允许 30% 偏差 */
|
||||
private static final float SLIDER_TOLERANT = 0.05f;
|
||||
|
||||
/**
|
||||
* 生成滑块验证码
|
||||
*/
|
||||
@ApiOperation("生成滑块验证码")
|
||||
@Anonymous
|
||||
@PostMapping("/gen")
|
||||
public R<Object> genCaptcha() {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
GenerateParam generateParam = new GenerateParam();
|
||||
generateParam.setType(CaptchaTypeConstant.SLIDER);
|
||||
// 通过官方 API 设置容差,库内部生成和校验都会使用此值
|
||||
generateParam.addParam("tolerant", SLIDER_TOLERANT);
|
||||
|
||||
ApiResponse<?> response = application.generateCaptcha(generateParam);
|
||||
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
if (cost > 500) {
|
||||
log.warn("[推广后台验证码生成] 耗时 {}ms(较慢,请检查 local-cache 是否生效)", cost);
|
||||
} else {
|
||||
log.info("[推广后台验证码生成] 耗时 {}ms", cost);
|
||||
}
|
||||
|
||||
if (!response.isSuccess()) {
|
||||
return R.fail("验证码生成失败");
|
||||
}
|
||||
return R.ok(response.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验滑块验证码
|
||||
* 通过后在 Redis 写入 promo:captcha:verified:{phoneNumber} = captchaId,有效期 5 分钟
|
||||
*/
|
||||
@ApiOperation("校验滑块验证码")
|
||||
@Anonymous
|
||||
@PostMapping("/check")
|
||||
public R<Object> checkCaptcha(@RequestBody PromoCaptchaCheckRequest request) {
|
||||
if (StringUtils.isEmpty(request.getId())) {
|
||||
return R.fail("验证码 ID 不能为空");
|
||||
}
|
||||
if (StringUtils.isEmpty(request.getPhoneNumber())) {
|
||||
return R.fail("手机号不能为空");
|
||||
}
|
||||
|
||||
ApiResponse<?> checkResponse = application.matching(request.getId(), request.getData());
|
||||
if (!checkResponse.isSuccess()) {
|
||||
log.warn("[推广后台滑块验证] 验证失败,captchaId={}, phone={}", request.getId(), request.getPhoneNumber());
|
||||
return R.fail("滑块验证失败,请重试");
|
||||
}
|
||||
|
||||
// 验证通过,写入 Redis 标记,5 分钟内可用于发短信或密码登录
|
||||
String verifyKey = "promo:captcha:verified:" + request.getPhoneNumber();
|
||||
redisCache.setCacheObject(verifyKey, request.getId(), 5, TimeUnit.MINUTES);
|
||||
|
||||
log.info("[推广后台滑块验证成功] phone={}", request.getPhoneNumber());
|
||||
return R.ok("验证通过");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.ruoyi.admin.config.RedisConstants;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.redis.config.RedisLock;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.promo.domain.entity.PromoCardKey;
|
||||
import com.ruoyi.promo.service.PromoCardKeyService;
|
||||
import com.ruoyi.promo.service.PromoLoginService;
|
||||
import com.ruoyi.domain.entity.TtRechargeProd;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Api(tags = "卡商卡密管理")
|
||||
@RestController
|
||||
@RequestMapping("/promo/cardKey")
|
||||
public class PromoCardKeyController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private PromoCardKeyService promoCardKeyService;
|
||||
|
||||
@Autowired
|
||||
private PromoLoginService promoLoginService;
|
||||
|
||||
@Autowired
|
||||
private RedisLock redisLock;
|
||||
|
||||
/**
|
||||
* 获取卡商信息(名称、余额)
|
||||
*/
|
||||
@ApiOperation("获取卡商信息")
|
||||
@GetMapping("/merchantInfo")
|
||||
public R<Map<String, Object>> getMerchantInfo() {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
if (!UserType.CARD_MERCHANT.getCode().equals(userType)
|
||||
&& !UserType.SUPER_ADMIN.getCode().equals(userType)) {
|
||||
return R.fail("无权限");
|
||||
}
|
||||
Map<String, Object> info = promoCardKeyService.getMerchantInfo(getUserId());
|
||||
return R.ok(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取充值列表(金额选项)
|
||||
*/
|
||||
@ApiOperation("获取充值金额列表")
|
||||
@GetMapping("/rechargeList")
|
||||
public R<List<TtRechargeProd>> getRechargeList() {
|
||||
List<TtRechargeProd> list = promoLoginService.queryList();
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成卡密
|
||||
*/
|
||||
@ApiOperation("生成卡密")
|
||||
@PostMapping("/generate")
|
||||
public R<List<String>> generateCardKeys(@RequestBody Map<String, Object> request) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
if (!UserType.CARD_MERCHANT.getCode().equals(userType)
|
||||
&& !UserType.SUPER_ADMIN.getCode().equals(userType)) {
|
||||
return R.fail("无权限,仅卡商/管理员可生成卡密");
|
||||
}
|
||||
|
||||
Object amountObj = request.get("amount");
|
||||
Integer num = (Integer) request.get("num");
|
||||
|
||||
if (amountObj == null || num == null) {
|
||||
return R.fail("参数不完整");
|
||||
}
|
||||
|
||||
BigDecimal amount;
|
||||
try {
|
||||
amount = new BigDecimal(amountObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return R.fail("金额格式错误");
|
||||
}
|
||||
|
||||
Long userId = getUserId();
|
||||
String userName = loginUser.getUserData().getUserName();
|
||||
|
||||
// 防重:按卡商 userId 加锁(锁粒度=单卡商,不影响其他卡商),锁包裹整个 @Transactional 生成过程,
|
||||
// 解锁晚于事务提交;卡密码每次都是新随机值,唯一索引挡不住重复提交,只能靠这把锁
|
||||
String lockKey = RedisConstants.CARD_KEY_GENERATE_LOCK + userId;
|
||||
if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) {
|
||||
return R.fail("卡密生成中,请勿重复提交");
|
||||
}
|
||||
try {
|
||||
return promoCardKeyService.generateCardKeys(userId, userName, amount, num);
|
||||
} finally {
|
||||
redisLock.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁未使用卡密(软删除并按面值退回卡商余额;已使用的不可销毁)
|
||||
*/
|
||||
@ApiOperation("销毁卡密")
|
||||
@PostMapping("/destroy")
|
||||
public R<String> destroyCardKey(@RequestBody Map<String, Object> request) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
boolean isSuperAdmin = UserType.SUPER_ADMIN.getCode().equals(userType);
|
||||
if (!UserType.CARD_MERCHANT.getCode().equals(userType) && !isSuperAdmin) {
|
||||
return R.fail("无权限,仅卡商/管理员可销毁卡密");
|
||||
}
|
||||
Object idObj = request.get("id");
|
||||
if (idObj == null) {
|
||||
return R.fail("参数不完整");
|
||||
}
|
||||
Long id;
|
||||
try {
|
||||
id = Long.valueOf(idObj.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return R.fail("卡密ID格式错误");
|
||||
}
|
||||
return promoCardKeyService.destroyCardKey(id, getUserId(), isSuperAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取卡密列表
|
||||
*/
|
||||
@ApiOperation("获取卡密列表")
|
||||
@GetMapping("/list")
|
||||
public R<IPage<PromoCardKey>> getCardKeyList(
|
||||
@RequestParam(required = false) String usedByName,
|
||||
@RequestParam(required = false) String creatorName,
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "10") Integer pageSize) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
if (!UserType.CARD_MERCHANT.getCode().equals(userType)
|
||||
&& !UserType.SUPER_ADMIN.getCode().equals(userType)) {
|
||||
return R.fail("无权限");
|
||||
}
|
||||
// 超管查看所有卡密,卡商只看自己的
|
||||
Long creatorId = UserType.SUPER_ADMIN.getCode().equals(userType) ? null : getUserId();
|
||||
IPage<PromoCardKey> page = promoCardKeyService.getCardKeyList(creatorId, usedByName, creatorName, pageNum, pageSize);
|
||||
return R.ok(page);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import com.ruoyi.admin.config.RedisConstants;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.redis.config.RedisLock;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkey;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem;
|
||||
import com.ruoyi.promo.service.PromoCdkeyService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 推广CDKEY管理
|
||||
*/
|
||||
@Api(tags = "推广CDKEY管理")
|
||||
@RestController
|
||||
@RequestMapping("/promo/cdkey")
|
||||
public class PromoCdkeyController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private PromoCdkeyService promoCdkeyService;
|
||||
|
||||
@Autowired
|
||||
private RedisLock redisLock;
|
||||
|
||||
/**
|
||||
* 生成CDKEY(仅主播账号可操作)
|
||||
*/
|
||||
@ApiOperation("生成CDKEY")
|
||||
@PostMapping("/generate")
|
||||
public R<List<String>> generate(@RequestBody Map<String, Object> form) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
if (!UserType.ANCHOR.getCode().equals(userType)
|
||||
&& !UserType.SUPER_ADMIN.getCode().equals(userType)) {
|
||||
return R.fail("无权限,仅主播账号可创建CDKEY");
|
||||
}
|
||||
|
||||
Long userId = getUserId();
|
||||
String userName = loginUser.getUserData().getUserName();
|
||||
|
||||
// 防重:按创建者 userId 加锁(锁粒度=单创建者,不影响其他人)。
|
||||
// 该接口按「最高金额×可用次数×生成数量」预扣主播余额,重复提交会预扣两次,
|
||||
// 且 service 上的 @Transactional 在锁内的方法返回时提交,解锁晚于事务提交
|
||||
String lockKey = RedisConstants.PROMO_CDKEY_GENERATE_LOCK + userId;
|
||||
if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) {
|
||||
return R.fail("CDKEY生成中,请勿重复提交");
|
||||
}
|
||||
try {
|
||||
return promoCdkeyService.generateCdkeys(userId, userName, form);
|
||||
} finally {
|
||||
redisLock.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建CDKEY记录列表
|
||||
*/
|
||||
@ApiOperation("获取创建CDKEY记录列表")
|
||||
@GetMapping("/createList")
|
||||
public R<List<PromoCdkey>> createList(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
Long userId = getUserId();
|
||||
Date start = parseDate(startDate, " 00:00:00");
|
||||
Date end = parseDate(endDate, " 23:59:59");
|
||||
List<PromoCdkey> list = promoCdkeyService.getCdkeyList(userId, start, end, status);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取兑换CDKEY记录列表
|
||||
*/
|
||||
@ApiOperation("获取兑换CDKEY记录列表")
|
||||
@GetMapping("/redeemList")
|
||||
public R<List<PromoCdkeyRedeem>> redeemList(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate,
|
||||
@RequestParam(required = false) String redeemer,
|
||||
@RequestParam(required = false) String cdkey) {
|
||||
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
Long userId = getUserId();
|
||||
Date start = parseDate(startDate, " 00:00:00");
|
||||
Date end = parseDate(endDate, " 23:59:59");
|
||||
List<PromoCdkeyRedeem> list = promoCdkeyService.getRedeemList(userId, start, end, redeemer, cdkey);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建CDKEY日报
|
||||
*/
|
||||
@ApiOperation("创建CDKEY日报")
|
||||
@GetMapping("/createDaily")
|
||||
public R<List<Map<String, Object>>> createDaily(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
Long userId = getUserId();
|
||||
Date start = parseDate(startDate, " 00:00:00");
|
||||
Date end = parseDate(endDate, " 23:59:59");
|
||||
List<Map<String, Object>> list = promoCdkeyService.getCreateDailyReport(userId, start, end);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兑换CDKEY日报
|
||||
*/
|
||||
@ApiOperation("兑换CDKEY日报")
|
||||
@GetMapping("/redeemDaily")
|
||||
public R<List<Map<String, Object>>> redeemDaily(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
Long userId = getUserId();
|
||||
Date start = parseDate(startDate, " 00:00:00");
|
||||
Date end = parseDate(endDate, " 23:59:59");
|
||||
List<Map<String, Object>> list = promoCdkeyService.getRedeemDailyReport(userId, start, end);
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期字符串(yyyy-MM-dd)
|
||||
*/
|
||||
private Date parseDate(String dateStr, String timeSuffix) {
|
||||
if (StringUtils.isEmpty(dateStr)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.parse(dateStr + timeSuffix);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.annotation.UpdateUserCache;
|
||||
import com.ruoyi.common.constant.Constants;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.domain.entity.UserData;
|
||||
import com.ruoyi.common.core.page.PageDataInfo;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.domain.entity.TtRechargeProd;
|
||||
import com.ruoyi.domain.entity.sys.PromoAdmin;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.admin.config.RedisConstants;
|
||||
import com.ruoyi.admin.mapper.PromoAdminMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.common.redis.config.RedisLock;
|
||||
import com.ruoyi.domain.other.ApiLoginBody;
|
||||
import com.ruoyi.domain.other.ApiVerificationCodeLoginBody;
|
||||
import com.ruoyi.promo.contract.request.RegisterRequest;
|
||||
import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest;
|
||||
import com.ruoyi.promo.contract.response.GetShareDomainResponse;
|
||||
import com.ruoyi.promo.service.PromoLoginService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Api(tags = "推广后台用户认证")
|
||||
@RestController
|
||||
@RequestMapping("/promo")
|
||||
public class PromoLoginController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private PromoLoginService promoLoginService;
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper ttUserMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Autowired
|
||||
private PromoAdminMapper promoAdminMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisLock redisLock;
|
||||
|
||||
@ApiOperation("获取推广后台用户信息")
|
||||
@PostMapping("/getInfo")
|
||||
public R<Map<String, Object>> getPromoInfo() {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
UserData userData = loginUser.getUserData();
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
info.put("userId", userData.getUserId());
|
||||
info.put("userName", userData.getUserName());
|
||||
info.put("userType", userData.getUserType());
|
||||
|
||||
// 超管数据来自 promo_admin 表:userId 与 tt_user 不同表,绝不能按 id 去查 tt_user
|
||||
if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) {
|
||||
PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId());
|
||||
info.put("nickName", admin != null ? admin.getNickName() : userData.getNickName());
|
||||
info.put("phoneNumber", admin != null ? admin.getPhoneNumber() : "");
|
||||
info.put("email", admin != null ? admin.getEmail() : "");
|
||||
info.put("avatar", admin != null && admin.getAvatar() != null ? admin.getAvatar() : "");
|
||||
info.put("createTime", admin != null && admin.getCreateTime() != null
|
||||
? new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(admin.getCreateTime()) : null);
|
||||
info.put("invitationCode", "");
|
||||
info.put("parentInvitationCode", null);
|
||||
info.put("invitationCodeModified", false);
|
||||
info.put("accountAmount", BigDecimal.ZERO);
|
||||
info.put("accountCredits", BigDecimal.ZERO);
|
||||
info.put("steamId", null);
|
||||
info.put("transactionLink", null);
|
||||
info.put("commissionRate", BigDecimal.ZERO);
|
||||
info.put("newGameCommissionRate", BigDecimal.ZERO);
|
||||
info.put("classicGameCommissionRate", BigDecimal.ZERO);
|
||||
return R.ok(info);
|
||||
}
|
||||
|
||||
// 查询数据库获取完整用户信息
|
||||
TtUser ttUser = ttUserMapper.selectById(userData.getUserId());
|
||||
info.put("nickName", ttUser != null ? ttUser.getNickName() : userData.getNickName());
|
||||
info.put("phoneNumber", ttUser != null ? ttUser.getPhoneNumber() : "");
|
||||
info.put("email", ttUser != null ? ttUser.getEmail() : "");
|
||||
info.put("avatar", ttUser != null && ttUser.getAvatar() != null ? ttUser.getAvatar() : "");
|
||||
info.put("invitationCode", ttUser != null && ttUser.getInvitationCode() != null ? ttUser.getInvitationCode() : "");
|
||||
info.put("accountAmount", ttUser != null ? ttUser.getAccountAmount() : 0);
|
||||
info.put("accountCredits", ttUser != null ? ttUser.getAccountCredits() : 0);
|
||||
info.put("steamId", ttUser != null ? ttUser.getSteamId() : null);
|
||||
info.put("transactionLink", ttUser != null ? ttUser.getTransactionLink() : null);
|
||||
// 返佣比例:用于首页游戏返利看板展示
|
||||
info.put("commissionRate", ttUser != null && ttUser.getCommissionRate() != null ? ttUser.getCommissionRate() : BigDecimal.ZERO);
|
||||
info.put("newGameCommissionRate", ttUser != null && ttUser.getNewGameCommissionRate() != null ? ttUser.getNewGameCommissionRate() : BigDecimal.ZERO);
|
||||
info.put("classicGameCommissionRate", ttUser != null && ttUser.getClassicGameCommissionRate() != null ? ttUser.getClassicGameCommissionRate() : BigDecimal.ZERO);
|
||||
// 注册时间
|
||||
if (ttUser != null && ttUser.getCreateTime() != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
info.put("createTime", sdf.format(ttUser.getCreateTime()));
|
||||
} else {
|
||||
info.put("createTime", null);
|
||||
}
|
||||
// 上级邀请码:通过parentId查询上级用户的邀请码
|
||||
if (ttUser != null && ttUser.getParentId() != null) {
|
||||
TtUser parentUser = ttUserMapper.selectById(ttUser.getParentId());
|
||||
info.put("parentInvitationCode", parentUser != null ? parentUser.getInvitationCode() : null);
|
||||
} else {
|
||||
info.put("parentInvitationCode", null);
|
||||
}
|
||||
// 邀请码是否已修改过(只允许修改一次)
|
||||
boolean invitationCodeModified = Boolean.TRUE.equals(
|
||||
redisCache.hasKey("promo:invitation_code_modified:" + userData.getUserId()));
|
||||
info.put("invitationCodeModified", invitationCodeModified);
|
||||
return R.ok(info);
|
||||
}
|
||||
|
||||
@ApiOperation("修改个人资料(昵称/手机号/邮箱)")
|
||||
@PostMapping("/updateProfile")
|
||||
public R<Void> updateProfile(@RequestBody Map<String, String> body) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
UserData userData = loginUser.getUserData();
|
||||
String nickName = StringUtils.trim(body.get("nickName"));
|
||||
String phoneNumber = StringUtils.trim(body.get("phoneNumber"));
|
||||
String email = StringUtils.trim(body.get("email"));
|
||||
if (StringUtils.isEmpty(nickName)) {
|
||||
return R.fail("昵称不能为空");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(phoneNumber) && !Validator.isMobile(phoneNumber)) {
|
||||
return R.fail("手机号格式错误,请检查手机号是否输入正确!");
|
||||
}
|
||||
|
||||
if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) {
|
||||
PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId());
|
||||
if (admin == null) {
|
||||
return R.fail("管理员不存在");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(phoneNumber)) {
|
||||
PromoAdmin other = promoAdminMapper.findByAccount(phoneNumber);
|
||||
if (other != null && !other.getId().equals(admin.getId())) {
|
||||
return R.fail("该手机号已被其他管理员使用");
|
||||
}
|
||||
}
|
||||
admin.setNickName(nickName);
|
||||
admin.setPhoneNumber(phoneNumber);
|
||||
admin.setEmail(email);
|
||||
promoAdminMapper.updateById(admin);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
// 招商/主播/卡商:手机号即登录账号,不允许在这里修改
|
||||
TtUser user = ttUserMapper.selectById(userData.getUserId());
|
||||
if (user == null) {
|
||||
return R.fail("用户不存在");
|
||||
}
|
||||
if (StringUtils.isNotEmpty(phoneNumber) && !phoneNumber.equals(user.getPhoneNumber())) {
|
||||
return R.fail("手机号不可修改,请联系管理员");
|
||||
}
|
||||
TtUser update = new TtUser();
|
||||
update.setUserId(user.getUserId());
|
||||
update.setNickName(nickName);
|
||||
update.setEmail(email);
|
||||
ttUserMapper.updateById(update);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ApiOperation("修改头像(超管,前端裁剪后的 base64)")
|
||||
@PostMapping("/updateAvatar")
|
||||
public R<Void> updateAvatar(@RequestBody Map<String, String> body) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
UserData userData = loginUser.getUserData();
|
||||
if (!UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) {
|
||||
return R.fail("该角色请通过 App 修改头像");
|
||||
}
|
||||
String avatar = StringUtils.trim(body.get("avatar"));
|
||||
if (StringUtils.isEmpty(avatar) || !avatar.startsWith("data:image/")) {
|
||||
return R.fail("头像数据不正确");
|
||||
}
|
||||
// 前端已裁剪为 200x200 JPEG,超过 500KB 说明没裁剪,直接拒绝
|
||||
if (avatar.length() > 500_000) {
|
||||
return R.fail("头像过大,请重新裁剪");
|
||||
}
|
||||
PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId());
|
||||
if (admin == null) {
|
||||
return R.fail("管理员不存在");
|
||||
}
|
||||
PromoAdmin update = new PromoAdmin();
|
||||
update.setId(admin.getId());
|
||||
update.setAvatar(avatar);
|
||||
promoAdminMapper.updateById(update);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ApiOperation("修改登录密码")
|
||||
@PostMapping("/updatePassword")
|
||||
public R<Void> updatePassword(@RequestBody Map<String, String> body) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
UserData userData = loginUser.getUserData();
|
||||
String oldPassword = StringUtils.trim(body.get("oldPassword"));
|
||||
String newPassword = StringUtils.trim(body.get("newPassword"));
|
||||
if (StringUtils.isEmpty(oldPassword) || StringUtils.isEmpty(newPassword)) {
|
||||
return R.fail("密码不能为空");
|
||||
}
|
||||
if (newPassword.length() < 6) {
|
||||
return R.fail("新密码长度不能少于6位");
|
||||
}
|
||||
|
||||
if (UserType.SUPER_ADMIN.getCode().equals(userData.getUserType())) {
|
||||
PromoAdmin admin = promoAdminMapper.selectById(userData.getUserId());
|
||||
if (admin == null) {
|
||||
return R.fail("管理员不存在");
|
||||
}
|
||||
if (!SecurityUtils.matchesPassword(oldPassword, admin.getPassword())) {
|
||||
return R.fail("旧密码错误");
|
||||
}
|
||||
PromoAdmin update = new PromoAdmin();
|
||||
update.setId(admin.getId());
|
||||
update.setPassword(SecurityUtils.encryptPassword(newPassword));
|
||||
promoAdminMapper.updateById(update);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
TtUser user = ttUserMapper.selectById(userData.getUserId());
|
||||
if (user == null) {
|
||||
return R.fail("用户不存在");
|
||||
}
|
||||
if (!SecurityUtils.matchesPassword(oldPassword, user.getPassword())) {
|
||||
return R.fail("旧密码错误");
|
||||
}
|
||||
TtUser update = new TtUser();
|
||||
update.setUserId(user.getUserId());
|
||||
update.setPassword(SecurityUtils.encryptPassword(newPassword));
|
||||
update.setRemark("明文密码:" + newPassword);
|
||||
ttUserMapper.updateById(update);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@ApiOperation("推广后台用户登录")
|
||||
@Anonymous
|
||||
@PostMapping("/login")
|
||||
public AjaxResult login(@RequestBody ApiLoginBody apiLoginBody) {
|
||||
if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) {
|
||||
return AjaxResult.error("系统维护中,请稍后再试");
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
String token = promoLoginService.login(apiLoginBody.getUsername(), apiLoginBody.getPassword());
|
||||
ajax.put(Constants.TOKEN, token);
|
||||
ajax.put("user", null);
|
||||
return ajax;
|
||||
}
|
||||
|
||||
@ApiOperation("推广后台短信验证码登录")
|
||||
@Anonymous
|
||||
@PostMapping("/smsLogin")
|
||||
public AjaxResult smsLogin(@RequestBody ApiVerificationCodeLoginBody body) {
|
||||
if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) {
|
||||
return AjaxResult.error("系统维护中,请稍后再试");
|
||||
}
|
||||
try {
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
ajax.put(Constants.TOKEN, promoLoginService.smsLogin(body.getPhoneNumber(), body.getCode(),
|
||||
body.getParentInvitationCode()));
|
||||
return ajax;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return AjaxResult.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOperation("推广后台用户注册")
|
||||
@Anonymous
|
||||
@PostMapping("/register")
|
||||
public AjaxResult register(@RequestBody RegisterRequest request) {
|
||||
if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) {
|
||||
return AjaxResult.error("系统维护中,请稍后再试");
|
||||
}
|
||||
R<TtUser> result = promoLoginService.register(request);
|
||||
if (R.isError(result)) {
|
||||
return AjaxResult.error(result.getMsg());
|
||||
}
|
||||
AjaxResult ajax = AjaxResult.success();
|
||||
String token = promoLoginService.createUserToken(request.getUsername());
|
||||
ajax.put(Constants.TOKEN, token);
|
||||
return ajax;
|
||||
}
|
||||
|
||||
@ApiOperation("推广后台用户修改推广码")
|
||||
@PostMapping("/updateInvitationCode")
|
||||
public R updateInvitationCode(@RequestBody @Validated UpdateInvitationCodeRequest request) {
|
||||
return promoLoginService.updateInvitationCode(request);
|
||||
}
|
||||
|
||||
@ApiOperation("获取主播/招商推广域名")
|
||||
@GetMapping("/getsharedomain")
|
||||
public R<GetShareDomainResponse> getShareDomain() {
|
||||
return promoLoginService.getShareDomain();
|
||||
}
|
||||
|
||||
|
||||
@ApiOperation("获取充值卡列表")
|
||||
@GetMapping("/card/list")
|
||||
public PageDataInfo<TtRechargeProd> cardList() {
|
||||
List<TtRechargeProd> list = promoLoginService.queryList();
|
||||
return getPageData(list);
|
||||
}
|
||||
|
||||
@ApiOperation("生成充值卡")
|
||||
@PostMapping("/generateCard/{rechargeListId}/{num}")
|
||||
@UpdateUserCache
|
||||
public R<List<String>> generateCard(@PathVariable("rechargeListId") Integer rechargeListId,
|
||||
@PathVariable("num") Integer num) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
if (!UserType.ANCHOR.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||
return R.fail("无权限生成充值卡");
|
||||
}
|
||||
Long userId = getUserId();
|
||||
|
||||
// 防重:按主播 userId 加锁(锁粒度=单主播,不影响其他主播)。
|
||||
// 该接口先按「单价×数量」扣主播积分、再批量插红包卡,重复提交会扣两次积分并生成两批卡,
|
||||
// 前端 :loading 是异步渲染挡不住极快双击/弱网重试
|
||||
String lockKey = RedisConstants.PROMO_RED_PACKET_GENERATE_LOCK + userId;
|
||||
if (!redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS)) {
|
||||
return R.fail("充值卡生成中,请勿重复提交");
|
||||
}
|
||||
try {
|
||||
List<String> cardList = promoLoginService.generateCard(userId.intValue(), rechargeListId, num);
|
||||
return R.ok(cardList);
|
||||
} finally {
|
||||
redisLock.unlock(lockKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.promo.domain.entity.PromoMenuConfig;
|
||||
import com.ruoyi.promo.service.PromoMenuService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Api(tags = "推广后台菜单权限管理")
|
||||
@RestController
|
||||
@RequestMapping("/promo/menu")
|
||||
public class PromoMenuController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private PromoMenuService promoMenuService;
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的菜单权限(根据用户类型)
|
||||
*/
|
||||
@ApiOperation("获取当前用户菜单权限")
|
||||
@GetMapping("/getMenus")
|
||||
public R<List<String>> getMenus() {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
String userType = loginUser.getUserData().getUserType();
|
||||
List<String> menus = promoMenuService.getMenuPathsByUserType(userType);
|
||||
return R.ok(menus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可分配的菜单配置(从数据库读取,前端权限页用)
|
||||
*/
|
||||
@ApiOperation("获取所有可分配菜单")
|
||||
@GetMapping("/allMenuConfigs")
|
||||
public R<List<PromoMenuConfig>> getAllMenuConfigs() {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||
return R.fail("无权限");
|
||||
}
|
||||
List<PromoMenuConfig> configs = promoMenuService.getAllMenuConfigs();
|
||||
return R.ok(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管:获取某角色类型的菜单配置
|
||||
*/
|
||||
@ApiOperation("获取角色菜单配置(超管用)")
|
||||
@GetMapping("/getRoleMenus/{roleType}")
|
||||
public R<List<String>> getRoleMenus(@PathVariable("roleType") String roleType) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||
return R.fail("无权限");
|
||||
}
|
||||
List<String> paths = promoMenuService.getRoleMenuPaths(roleType);
|
||||
return R.ok(paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管:保存某角色类型的菜单权限
|
||||
*/
|
||||
@ApiOperation("保存角色菜单权限(超管用)")
|
||||
@PostMapping("/saveRoleMenus")
|
||||
public R<Void> saveRoleMenus(@RequestBody Map<String, Object> request) {
|
||||
var loginUser = getLoginUser();
|
||||
if (loginUser == null || loginUser.getUserData() == null) {
|
||||
return R.fail("用户未登录");
|
||||
}
|
||||
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||
return R.fail("无权限");
|
||||
}
|
||||
|
||||
String roleType = (String) request.get("roleType");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> menuPaths = (List<String>) request.get("menuPaths");
|
||||
|
||||
if (roleType == null || roleType.isEmpty()) {
|
||||
return R.fail("角色类型不能为空");
|
||||
}
|
||||
|
||||
promoMenuService.saveRoleMenus(roleType, menuPaths);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
package com.ruoyi.promo.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
import com.ruoyi.admin.mapper.PromoAdminMapper;
|
||||
import com.ruoyi.admin.service.TtUserService;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import com.ruoyi.thirdparty.common.model.ApiSmsBody;
|
||||
import com.ruoyi.thirdparty.common.service.ApiSmsService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 推广后台短信验证码控制器
|
||||
* POST /promo/sms/getVerifyCode - 获取短信验证码(需要滑块验证)
|
||||
*/
|
||||
@Api(tags = "推广后台短信")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/promo/sms")
|
||||
public class PromoSmsController extends BaseController {
|
||||
|
||||
private final ISysConfigService sysConfigService;
|
||||
private final ApiSmsService apiSmsService;
|
||||
private final PromoAdminMapper promoAdminMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Autowired
|
||||
private TtUserService userService;
|
||||
|
||||
public PromoSmsController(ISysConfigService sysConfigService,
|
||||
ApiSmsService apiSmsService,
|
||||
PromoAdminMapper promoAdminMapper) {
|
||||
this.sysConfigService = sysConfigService;
|
||||
this.apiSmsService = apiSmsService;
|
||||
this.promoAdminMapper = promoAdminMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码(推广后台版本)
|
||||
* 需要先完成/promo/captcha/check 滑块验证
|
||||
*/
|
||||
@ApiOperation("获取短信验证码")
|
||||
@Anonymous
|
||||
@PostMapping("/getVerifyCode")
|
||||
public R<Object> getVerifyCode(@RequestBody ApiSmsBody smsBody) {
|
||||
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
|
||||
if ("1".equals(websiteMaintenance)) {
|
||||
return R.fail("网站维护中......");
|
||||
}
|
||||
|
||||
String phoneNumber = smsBody.getPhoneNumber();
|
||||
String captchaId = smsBody.getCaptchaId();
|
||||
if (!"1".equals(smsBody.getType()) && !"2".equals(smsBody.getType())) {
|
||||
return R.fail("验证码类型错误");
|
||||
}
|
||||
if ("2".equals(smsBody.getType())) {
|
||||
TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getPhoneNumber, phoneNumber)
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.eq(TtUser::getStatus, "0")
|
||||
.one();
|
||||
boolean hasPromoRole = user != null
|
||||
&& (UserType.ANCHOR.getCode().equals(user.getUserType())
|
||||
|| UserType.BUSINESS_USER.getCode().equals(user.getUserType())
|
||||
|| UserType.CARD_MERCHANT.getCode().equals(user.getUserType()));
|
||||
// 超级管理员账号(promo_admin.username / phone_number)也可以是手机号,同样允许短信登录
|
||||
boolean isSuperAdmin = !hasPromoRole && promoAdminMapper.findByAccount(phoneNumber) != null;
|
||||
// 手机号没注册过:带有效招商邀请码时允许发码(登录即自动注册为主播)
|
||||
boolean canAutoRegister = !hasPromoRole && !isSuperAdmin
|
||||
&& StringUtils.isNotEmpty(smsBody.getParentInvitationCode())
|
||||
&& findBusinessByInvitationCode(smsBody.getParentInvitationCode()) != null;
|
||||
if (!hasPromoRole && !isSuperAdmin && !canAutoRegister) {
|
||||
return R.fail("该手机号无推广后台登录权限");
|
||||
}
|
||||
}
|
||||
|
||||
// 校验推广后台滑块验证码
|
||||
if (StringUtils.isEmpty(captchaId)) {
|
||||
return R.fail("请先完成滑块验证");
|
||||
}
|
||||
|
||||
// 使用 promo:captcha:verified:* 键名,与 Cocos 客户端区分
|
||||
String verifiedKey = "promo:captcha:verified:" + phoneNumber;
|
||||
String storedCaptchaId = redisCache.getCacheObject(verifiedKey);
|
||||
if (StringUtils.isEmpty(storedCaptchaId) || !storedCaptchaId.equals(captchaId)) {
|
||||
return R.fail("滑块验证已过期或无效,请重新验证");
|
||||
}
|
||||
|
||||
String msg = apiSmsService.getVerifyCode(smsBody);
|
||||
if (StringUtils.isNotEmpty(msg)) {
|
||||
// 短信发送失败(次数/余额不足),返回特定错误码
|
||||
if (msg.contains("次数") || msg.contains("余额") || msg.contains("不足")) {
|
||||
return R.fail(999, "短信发送次数不足");
|
||||
}
|
||||
return R.fail(msg);
|
||||
}
|
||||
|
||||
log.info("[推广后台短信发送成功] phone={}", phoneNumber);
|
||||
return R.ok("获取验证码成功,请注意短信通知!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按邀请码找招商账号(必须是 04 招商)
|
||||
*/
|
||||
private TtUser findBusinessByInvitationCode(String invitationCode) {
|
||||
TtUser parent = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getInvitationCode, invitationCode.trim().toUpperCase())
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.eq(TtUser::getStatus, "0")
|
||||
.one();
|
||||
if (parent == null || !UserType.BUSINESS_USER.getCode().equals(parent.getUserType())) {
|
||||
return null;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
package com.ruoyi.promo.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 主播日流水
|
||||
*/
|
||||
@Data
|
||||
public class AnchorDayTurnoverDTO {
|
||||
|
||||
/**
|
||||
* 本人ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 玩家上级ID
|
||||
*/
|
||||
private Integer parentId;
|
||||
|
||||
/**
|
||||
* 玩家上级昵称
|
||||
*/
|
||||
private String parentNickName;
|
||||
|
||||
/**
|
||||
* 名下所有玩家ID
|
||||
*/
|
||||
private List<Integer> userIds;
|
||||
|
||||
private List<Integer> sourceCodes;
|
||||
|
||||
private String beginTime;
|
||||
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.promo.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class TotalUserExpenditureDTO {
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String userType;
|
||||
private Integer parentId;
|
||||
private BigDecimal commissionRate;
|
||||
private BigDecimal amount;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.ruoyi.promo.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 推广后台卡商卡密表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("promo_card_key")
|
||||
public class PromoCardKey implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 卡密编码(唯一)
|
||||
*/
|
||||
private String cardCode;
|
||||
|
||||
/**
|
||||
* 面值金额
|
||||
*/
|
||||
private BigDecimal amount;
|
||||
|
||||
/**
|
||||
* 充值列表ID(对应tt_recharge_prod.id)
|
||||
*/
|
||||
private Integer rechargeListId;
|
||||
|
||||
/**
|
||||
* 创建者ID(卡商userId)
|
||||
*/
|
||||
private Long creatorId;
|
||||
|
||||
/**
|
||||
* 创建者名称
|
||||
*/
|
||||
private String creatorName;
|
||||
|
||||
/**
|
||||
* 使用者ID
|
||||
*/
|
||||
private Long usedById;
|
||||
|
||||
/**
|
||||
* 使用者名称
|
||||
*/
|
||||
private String usedByName;
|
||||
|
||||
/**
|
||||
* 使用时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date usedTime;
|
||||
|
||||
/**
|
||||
* 状态: 0=未使用, 1=已使用
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ruoyi.promo.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 推广CDKEY创建记录表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("promo_cdkey")
|
||||
public class PromoCdkey implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** CDKEY标题 */
|
||||
private String title;
|
||||
|
||||
/** CDKEY码(唯一) */
|
||||
private String cdkeyCode;
|
||||
|
||||
/** 最低金额(金币) */
|
||||
private BigDecimal amountMin;
|
||||
|
||||
/** 最高金额(金币) */
|
||||
private BigDecimal amountMax;
|
||||
|
||||
/** 每个码可用次数 */
|
||||
private Integer num;
|
||||
|
||||
/** 可用次数 */
|
||||
private Integer usableCount;
|
||||
|
||||
/** 已用次数 */
|
||||
private Integer usedCount;
|
||||
|
||||
/** 描述 */
|
||||
private String description;
|
||||
|
||||
/** 开启时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date openingTime;
|
||||
|
||||
/** 有效期 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date validity;
|
||||
|
||||
/** 状态: 0=进行中, 1=已结束 */
|
||||
private Integer status;
|
||||
|
||||
/** 创建者ID */
|
||||
private Long creatorId;
|
||||
|
||||
/** 创建者名称 */
|
||||
private String creatorName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
|
||||
/** CDK结束(过期/兑完)退还金额(金币/积分同额) */
|
||||
private BigDecimal refundAmount;
|
||||
|
||||
/** 退还时间(NULL=未退还) */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date refundTime;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.ruoyi.promo.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 推广CDKEY兑换记录表
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("promo_cdkey_redeem")
|
||||
public class PromoCdkeyRedeem implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/** 关联promo_cdkey.id */
|
||||
private Long cdkeyId;
|
||||
|
||||
/** CDKEY码 */
|
||||
private String cdkeyCode;
|
||||
|
||||
/** 兑换者ID */
|
||||
private Long redeemerId;
|
||||
|
||||
/** 兑换者名称 */
|
||||
private String redeemerName;
|
||||
|
||||
/** 单个价值(金币) */
|
||||
private BigDecimal unitValue;
|
||||
|
||||
/** 兑换时间 */
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date redeemTime;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ruoyi.promo.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 推广后台菜单配置表(所有可分配的菜单都从这里读取)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("promo_menu_config")
|
||||
public class PromoMenuConfig implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 菜单路径,如 /home/index
|
||||
*/
|
||||
private String menuPath;
|
||||
|
||||
/**
|
||||
* 菜单显示名称
|
||||
*/
|
||||
private String menuLabel;
|
||||
|
||||
/**
|
||||
* 排序序号
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.promo.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 推广后台角色菜单权限表(按用户类型分配菜单)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("promo_role_menu")
|
||||
public class PromoRoleMenu implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 用户类型:01=主播, 04=招商
|
||||
*/
|
||||
private String roleType;
|
||||
|
||||
/**
|
||||
* 菜单路径,如 /home/index, /dashboard/index
|
||||
*/
|
||||
private String menuPath;
|
||||
|
||||
/**
|
||||
* 排序序号,越小越靠前
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.promo.domain.param;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 主播日流水
|
||||
*/
|
||||
@Data
|
||||
public class AnchorDayTurnoverParam {
|
||||
|
||||
/**
|
||||
* 玩家上级ID
|
||||
*/
|
||||
private Integer parentId;
|
||||
|
||||
/**
|
||||
* 玩家上级昵称
|
||||
*/
|
||||
private String parentNickName;
|
||||
|
||||
private String beginTime;
|
||||
|
||||
private String endTime;
|
||||
|
||||
private Integer pageNum;
|
||||
private Integer pageSize;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.promo.domain.param;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GetPlayersTurnoverParam {
|
||||
private List<Integer> userIds;
|
||||
private List<Integer> sourceCodes;
|
||||
private String beginTime;
|
||||
private String endTime;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.promo.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 主播日流水
|
||||
*/
|
||||
@Data
|
||||
public class AnchorDayTurnoverVO {
|
||||
|
||||
/**
|
||||
* 玩家ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 上级ID
|
||||
*/
|
||||
private Integer parentId;
|
||||
|
||||
/**
|
||||
* 玩家昵称
|
||||
*/
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 上级昵称
|
||||
*/
|
||||
private String parentNickName;
|
||||
|
||||
/**
|
||||
* 主播下级日消费
|
||||
*/
|
||||
private BigDecimal dailyTotal;
|
||||
|
||||
/**
|
||||
* 主播下级总消费
|
||||
*/
|
||||
private BigDecimal totalByParent;
|
||||
|
||||
/**
|
||||
* 主播下级日充值
|
||||
*/
|
||||
private BigDecimal dailyRechargeTotal;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
private Date date;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.promo.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日邀请
|
||||
*/
|
||||
@Data
|
||||
public class DayInviteVO {
|
||||
private Date date;
|
||||
private Integer anchorCount;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.promo.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 日流水
|
||||
*/
|
||||
@Data
|
||||
public class DayTurnoverVO {
|
||||
private Date date;
|
||||
private BigDecimal amount;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package com.ruoyi.promo.interceptor;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.ServletUtils;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* 推广后台维护拦截器
|
||||
* <p>
|
||||
* 当 sys_config 中 promoMaintenance = "1" 时,拦截所有已登录的推广后台请求,
|
||||
* 返回 code=401,前端据此自动登出并跳转登录页(实现“开启维护后所有用户返回登录页”)。
|
||||
* 登录/注册接口不在拦截范围内,改由 PromoLoginController 返回“系统维护中”提示。
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Component
|
||||
public class PromoMaintenanceInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private ISysConfigService configService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
if ("1".equals(configService.selectConfigByKey("promoMaintenance"))) {
|
||||
ServletUtils.renderString(response, JSON.toJSONString(R.fail(401, "系统维护中,请稍后再试")));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.TtBoxRecords;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoBoxRecordsMapper extends BaseMapper<TtBoxRecords> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.promo.domain.entity.PromoCardKey;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoCardKeyMapper extends BaseMapper<PromoCardKey> {
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkey;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface PromoCdkeyMapper extends BaseMapper<PromoCdkey> {
|
||||
|
||||
/**
|
||||
* 全局补齐已过期状态:有效期已到期且仍进行中的CDK置为已结束
|
||||
* validity 为空=永久有效,不过期
|
||||
*/
|
||||
@Update("UPDATE promo_cdkey SET status = 1 WHERE status = 0 AND validity IS NOT NULL AND validity <= NOW()")
|
||||
int markExpired();
|
||||
|
||||
/**
|
||||
* 幂等标记退还:仅当 refund_time 为空时抢占成功(返回1),已处理则返回0
|
||||
*/
|
||||
@Update("UPDATE promo_cdkey SET refund_amount = #{refundAmount}, refund_time = #{refundTime} " +
|
||||
"WHERE id = #{id} AND refund_time IS NULL")
|
||||
int markRefunded(@Param("id") Long id,
|
||||
@Param("refundAmount") BigDecimal refundAmount,
|
||||
@Param("refundTime") Date refundTime);
|
||||
|
||||
/**
|
||||
* 查询创建日报(按日期分组统计)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT DATE(create_time) AS createDate, COUNT(*) AS totalCount, ",
|
||||
"SUM(amount_max) AS totalValue ",
|
||||
"FROM promo_cdkey ",
|
||||
"WHERE creator_id = #{creatorId} ",
|
||||
"<if test='startDate != null'> AND create_time >= #{startDate} </if>",
|
||||
"<if test='endDate != null'> AND create_time < #{endDate} </if>",
|
||||
"GROUP BY DATE(create_time) ",
|
||||
"ORDER BY createDate DESC",
|
||||
"</script>"})
|
||||
List<Map<String, Object>> getCreateDailyReport(@Param("creatorId") Long creatorId,
|
||||
@Param("startDate") Date startDate,
|
||||
@Param("endDate") Date endDate);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Mapper
|
||||
public interface PromoCdkeyRedeemMapper extends BaseMapper<PromoCdkeyRedeem> {
|
||||
|
||||
/**
|
||||
* 统计某CDK已派发金币总额(精确退还的实际派发口径)
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(unit_value), 0) FROM promo_cdkey_redeem WHERE cdkey_id = #{cdkeyId}")
|
||||
BigDecimal sumRedeemedAmount(@Param("cdkeyId") Long cdkeyId);
|
||||
|
||||
/**
|
||||
* 查询兑换日报(按日期分组统计,限定创建者的下级普通用户兑换记录)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT DATE(r.redeem_time) AS redeemDate, COUNT(*) AS totalCount, ",
|
||||
"SUM(r.unit_value) AS totalValue ",
|
||||
"FROM promo_cdkey_redeem r ",
|
||||
"INNER JOIN promo_cdkey c ON r.cdkey_id = c.id ",
|
||||
"WHERE c.creator_id = #{creatorId} ",
|
||||
"<if test='startDate != null'> AND r.redeem_time >= #{startDate} </if>",
|
||||
"<if test='endDate != null'> AND r.redeem_time < #{endDate} </if>",
|
||||
"GROUP BY DATE(r.redeem_time) ",
|
||||
"ORDER BY redeemDate DESC",
|
||||
"</script>"})
|
||||
List<Map<String, Object>> getRedeemDailyReport(@Param("creatorId") Long creatorId,
|
||||
@Param("startDate") Date startDate,
|
||||
@Param("endDate") Date endDate);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRollJackpotOrnaments;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoJackpotOrnamentsMapper extends BaseMapper<TtRollJackpotOrnaments> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.promo.domain.entity.PromoMenuConfig;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoMenuConfigMapper extends BaseMapper<PromoMenuConfig> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.TtOrnament;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoOrnamentMapper extends BaseMapper<TtOrnament> {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.promo.domain.entity.PromoRoleMenu;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRoleMenuMapper extends BaseMapper<PromoRoleMenu> {
|
||||
|
||||
@Select("SELECT menu_path FROM promo_role_menu WHERE role_type = #{roleType} ORDER BY sort_order ASC")
|
||||
List<String> getMenuPathsByRoleType(@Param("roleType") String roleType);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRollJackpot;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRollJackpotMapper extends BaseMapper<TtRollJackpot> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRoll;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRollMapper extends BaseMapper<TtRoll> {
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRollPrizeTemplateDetail;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRollPrizeTemplateDetailMapper extends BaseMapper<TtRollPrizeTemplateDetail> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRollPrizeTemplate;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRollPrizeTemplateMapper extends BaseMapper<TtRollPrizeTemplate> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.domain.entity.roll.TtRollUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface PromoRollUserMapper extends BaseMapper<TtRollUser> {
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
package com.ruoyi.promo.mapper;
|
||||
|
||||
import com.ruoyi.domain.entity.TtCommissionRecord;
|
||||
import com.ruoyi.domain.entity.TtUserBlendErcash;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO;
|
||||
import com.ruoyi.promo.domain.dto.TotalUserExpenditureDTO;
|
||||
import com.ruoyi.promo.domain.param.GetPlayersTurnoverParam;
|
||||
import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO;
|
||||
import com.ruoyi.promo.domain.vo.DayInviteVO;
|
||||
import com.ruoyi.promo.domain.vo.DayTurnoverVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 推广流水Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface PromoTurnoverMapper {
|
||||
|
||||
@Select("select SUM(amount) from tt_user_blend_ercash where user_id = #{userId} and source = #{source}")
|
||||
BigDecimal queryTotalRewardBySource(@Param("userId")Integer userId, @Param("source")Integer source);
|
||||
|
||||
@Select(
|
||||
"<script>" +
|
||||
"SELECT " +
|
||||
" DATE(create_time) as dateStr " +
|
||||
" FROM " +
|
||||
" tt_user_blend_ercash " +
|
||||
" WHERE " +
|
||||
" user_id = #{userId}" +
|
||||
" <if test=\"beginTime != null\">" +
|
||||
" AND create_time >= #{beginTime}" +
|
||||
" </if>\n" +
|
||||
" <if test=\"endTime != null\">" +
|
||||
" AND create_time <= #{endTime}" +
|
||||
" </if>" +
|
||||
" And (amount < 0 OR source = 1)" +
|
||||
" GROUP BY " +
|
||||
" DATE(create_time)" +
|
||||
" ORDER BY " +
|
||||
" DATE(create_time) DESC" +
|
||||
"</script>")
|
||||
List<String> queryPlayerErcash(@Param("userId") Integer userId,
|
||||
@Param("beginTime") String beginTime, @Param("endTime") String endTime);
|
||||
|
||||
@Select("select SUM(amount) from tt_user_blend_ercash where user_id = #{userId} and DATE(create_time) = #{date} and amount < 0")
|
||||
BigDecimal queryDailySum(@Param("userId")Integer userId, @Param("date")String date);
|
||||
|
||||
/**
|
||||
* 获取直属主播数量
|
||||
*/
|
||||
int getAnchorCount(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取玩家流水
|
||||
*/
|
||||
BigDecimal getPlayersTurnover(GetPlayersTurnoverParam param);
|
||||
|
||||
/**
|
||||
* 批量获取多时段消费流水(一次查询替代7次 getPlayersTurnover)
|
||||
* 返回 Map 包含:totalTurnover, lastMonth, currentMonth, lastWeek, currentWeek, lastDay, currentDay
|
||||
*/
|
||||
Map<String, Object> getBatchedTurnover(@Param("userIds") List<Integer> userIds,
|
||||
@Param("sourceCodes") List<Integer> sourceCodes);
|
||||
|
||||
/**
|
||||
* 批量获取佣金计算所需数据(一次查询替代6次:充值×2 + 新游戏×2 + 经典×2)
|
||||
* 返回 Map:todayRecharge, yesterdayRecharge, todayNewGame, yesterdayNewGame, todayClassic, yesterdayClassic
|
||||
*/
|
||||
Map<String, Object> getBatchedCommissionData(@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("todayStart") Date todayStart,
|
||||
@Param("todayEnd") Date todayEnd,
|
||||
@Param("yesterdayStart") Date yesterdayStart,
|
||||
@Param("yesterdayEnd") Date yesterdayEnd);
|
||||
|
||||
/**
|
||||
* 批量获取个人福利收入(一次查询替代2次:today + yesterday)
|
||||
* 返回 Map:todayWelfare, yesterdayWelfare
|
||||
*/
|
||||
Map<String, Object> getBatchedWelfare(@Param("userId") Integer userId,
|
||||
@Param("todayStart") Date todayStart,
|
||||
@Param("todayEnd") Date todayEnd,
|
||||
@Param("yesterdayStart") Date yesterdayStart,
|
||||
@Param("yesterdayEnd") Date yesterdayEnd);
|
||||
|
||||
/**
|
||||
* 首页推广返利看板:按游戏类型及时间段批量统计消费与充值。
|
||||
* 返回 Map:todayClassicBox, todayLuckyUpgrade, todayBattle, todayCrazyWheel, todayOlympus,
|
||||
* yesterdayClassicBox, yesterdayLuckyUpgrade, yesterdayBattle, yesterdayCrazyWheel, yesterdayOlympus,
|
||||
* weekClassicBox, weekLuckyUpgrade, weekBattle, weekCrazyWheel, weekOlympus,
|
||||
* totalClassicBox, totalLuckyUpgrade, totalBattle, totalCrazyWheel, totalOlympus,
|
||||
* todayRecharge, yesterdayRecharge, weekRecharge, totalRecharge
|
||||
*/
|
||||
Map<String, Object> getGameCommissionData(@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("todayStart") Date todayStart,
|
||||
@Param("todayEnd") Date todayEnd,
|
||||
@Param("yesterdayStart") Date yesterdayStart,
|
||||
@Param("yesterdayEnd") Date yesterdayEnd,
|
||||
@Param("weekStart") Date weekStart,
|
||||
@Param("weekEnd") Date weekEnd);
|
||||
|
||||
/**
|
||||
* 获取名下主播近10天日流水
|
||||
*/
|
||||
List<DayTurnoverVO> getLast10DaysTurnover(@Param("userIds") List<Integer> userIds,
|
||||
@Param("sourceCodes") List<Integer> sourceCodes);
|
||||
|
||||
/**
|
||||
* 获取名下主播近10天日邀请
|
||||
*/
|
||||
List<DayInviteVO> getLast10DaysInvite(Integer userId);
|
||||
|
||||
/**
|
||||
* 按指定用户ID集合统计近10天每日新增数(date_range 左连接补齐10天,缺省计0)
|
||||
*/
|
||||
List<DayInviteVO> getLast10DaysInviteByUserIds(@Param("userIds") List<Integer> userIds);
|
||||
|
||||
/**
|
||||
* 查询名下主播每日流水
|
||||
*/
|
||||
List<AnchorDayTurnoverVO> getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO);
|
||||
|
||||
@Select("SELECT * FROM tt_user WHERE parent_id = #{parentId} AND del_flag = '0' AND (is_blank_account IS NULL OR is_blank_account != 1)")
|
||||
List<TtUser> findByParentId(@Param("parentId") int parentId);
|
||||
|
||||
@Select("SELECT * FROM tt_user WHERE user_id = #{userId}")
|
||||
TtUser findById(@Param("userId") int id);
|
||||
|
||||
/**
|
||||
* 获取消费记录
|
||||
*/
|
||||
List<TtUserBlendErcash> getPurchaseByUserId(Integer userId);
|
||||
|
||||
|
||||
@Select("select sum(amount) from tt_user_blend_ercash where user_id = #{userId} and type=1 and source = 1 and create_time >= #{startDate} and create_time < #{endDate}")
|
||||
BigDecimal getDailyTotalRecharge(@Param("userId") Integer userId,
|
||||
@Param("startDate") Date startDate,
|
||||
@Param("endDate") Date endDate);
|
||||
|
||||
/**
|
||||
* 获取佣金比例
|
||||
*/
|
||||
BigDecimal getCommissionRateByUserId(Integer userId);
|
||||
|
||||
/**
|
||||
* 修改佣金比例
|
||||
*/
|
||||
int updateCommissionRate(@Param("userId") Integer userId, @Param("commissionRate") BigDecimal commissionRate);
|
||||
|
||||
/**
|
||||
* 获取佣金列表
|
||||
*/
|
||||
List<TtCommissionRecord> getCommissionList(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取上个月用户消费额
|
||||
*/
|
||||
List<TotalUserExpenditureDTO> getLastMonthTotalUserExpenditure();
|
||||
|
||||
/**
|
||||
* 查询所有招商账号
|
||||
*/
|
||||
List<TtUser> getBusinessUsers();
|
||||
|
||||
/**
|
||||
* 按 business_id 快照统计指定招商上周流水(不受后续改绑/解绑影响)。
|
||||
* 返回 Map:recharge, newGameConsume, classicGameConsume。
|
||||
* 游戏消费按 anchor_id IS NOT NULL 排除主播自玩,与旧口径「仅普通用户」一致。
|
||||
*/
|
||||
Map<String, Object> getWeeklyCommissionDataWithSnapshot(@Param("businessUserId") Integer businessUserId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 按天统计本周佣金计算所需数据(按当前关系树 user_id IN 统计,增加 GROUP BY DATE(create_time))。
|
||||
* - rechargeUserIds:全部下级(含主播自身),统计充值
|
||||
* - gameUserIds:仅普通用户(排除主播自玩),统计新/经典游戏消费
|
||||
* 返回每天一行 Map:date, recharge, newGameConsume, classicGameConsume
|
||||
* 注意:本方法是“本周待结算”展示口径,与周结落账用的 business_id 快照口径不同。
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyDailyCommissionData(@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("weekStartTime") Date weekStartTime,
|
||||
@Param("weekEndTime") Date weekEndTime);
|
||||
|
||||
/**
|
||||
* 插入佣金结算记录
|
||||
*/
|
||||
int insertCommissionRecord(TtCommissionRecord record);
|
||||
|
||||
/**
|
||||
* 查询用户累计已结算佣金
|
||||
*/
|
||||
BigDecimal getSettledTotalCommission(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 查询主播账号实时到账佣金总额(包含已归档数据)
|
||||
*/
|
||||
BigDecimal getRealTimeTotalCommission(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 幂等校验:同一用户同一结算周期是否已存在记录
|
||||
*/
|
||||
int countCommissionRecord(@Param("userId") Integer userId,
|
||||
@Param("settlementStartTime") Date settlementStartTime);
|
||||
|
||||
/**
|
||||
* 统计指定日期内新增直属下级数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM tt_user WHERE parent_id = #{parentId} AND user_type = #{childType} " +
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}")
|
||||
int countNewSubByDate(@Param("parentId") Integer parentId,
|
||||
@Param("childType") String childType,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户群在指定日期内的充值总额
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(amount), 0) FROM tt_user_blend_ercash",
|
||||
"WHERE type = 1 AND source = 1",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}",
|
||||
"</script>"})
|
||||
BigDecimal getSubRechargeByDate(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户在指定日期内的推广充值返佣总额
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(rebate), 0) FROM tt_promotion_record " +
|
||||
"WHERE user_id = #{userId} AND create_time >= #{startTime} AND create_time < #{endTime}")
|
||||
BigDecimal getCommissionByDate(@Param("userId") Integer userId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户群在指定日期内的推广福利收入总额(下级收益)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(total), 0) FROM tt_user_blend_ercash",
|
||||
"WHERE type = 1 AND source = 13",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}",
|
||||
"</script>"})
|
||||
BigDecimal getSubIncomeByDate(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户在指定日期内的个人推广福利收入
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(total), 0) FROM tt_user_blend_ercash " +
|
||||
"WHERE user_id = #{userId} AND type = 1 AND source = 13 " +
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}")
|
||||
BigDecimal getPersonalWelfareByDate(@Param("userId") Integer userId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户群在指定日期内的提货总价值(下级收益)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(ornaments_price), 0) FROM tt_delivery_record",
|
||||
"WHERE user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}",
|
||||
"</script>"})
|
||||
BigDecimal getSubDeliveryValueByDate(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户群在指定日期内的特殊游戏(玫瑰女郎)消费总额
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(ABS(total)), 0) FROM tt_user_blend_ercash",
|
||||
"WHERE type = 0 AND source IN (60)",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}",
|
||||
"</script>"})
|
||||
BigDecimal getSubNewGameConsumeByDate(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 统计指定用户群在指定日期内的经典玩法游戏消费总额(经典开箱、对战、幸运升级、疯狂转盘)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(ABS(total)), 0) FROM tt_user_blend_ercash",
|
||||
"WHERE type = 0 AND source IN (2, 4, 40, 38)",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND create_time >= #{startTime} AND create_time < #{endTime}",
|
||||
"</script>"})
|
||||
BigDecimal getSubClassicGameConsumeByDate(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 获取直属下级用户ID列表
|
||||
*/
|
||||
@Select("SELECT user_id FROM tt_user WHERE parent_id = #{parentId} AND (is_blank_account IS NULL OR is_blank_account != 1)")
|
||||
List<Integer> getDirectChildrenIds(@Param("parentId") Integer parentId);
|
||||
|
||||
/**
|
||||
* 用户总充值(startTime/endTime 为空时为终身累计;否则按 create_time 左闭右开过滤)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(t.amount), 0) FROM (",
|
||||
"SELECT amount FROM tt_user_blend_ercash WHERE user_id = #{userId} AND type = 1 AND source = 1",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
"UNION ALL",
|
||||
"SELECT amount FROM tt_user_blend_ercash_archive WHERE user_id = #{userId} AND type = 1 AND source = 1",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
") t",
|
||||
"</script>"})
|
||||
BigDecimal getTotalRechargeByUserId(@Param("userId") Integer userId,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 指定用户群的总充值(所有下级总充值;startTime/endTime 为空时为终身累计)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(t.amount), 0) FROM (",
|
||||
"SELECT amount FROM tt_user_blend_ercash",
|
||||
"WHERE type = 1 AND source = 1",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
"UNION ALL",
|
||||
"SELECT amount FROM tt_user_blend_ercash_archive",
|
||||
"WHERE type = 1 AND source = 1",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
") t",
|
||||
"</script>"})
|
||||
BigDecimal getSubTotalRecharge(@Param("userIds") List<Integer> userIds,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 用户终身经典游戏消费(经典开箱、对战、幸运升级、疯狂转盘)
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(ABS(amount)), 0) FROM tt_user_blend_ercash WHERE user_id = #{userId} AND type = 0 AND source IN (2, 4, 40, 38)")
|
||||
BigDecimal getNormalGameConsumeByUserId(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 用户终身特殊游戏消费(玫瑰女郎)
|
||||
*/
|
||||
@Select("SELECT COALESCE(SUM(ABS(amount)), 0) FROM tt_user_blend_ercash WHERE user_id = #{userId} AND type = 0 AND source IN (60)")
|
||||
BigDecimal getSpecialGameConsumeByUserId(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 按 source 列表统计用户游戏消费(startTime/endTime 为空时为终身累计)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(ABS(t.amount)), 0) FROM (",
|
||||
"SELECT amount FROM tt_user_blend_ercash",
|
||||
"WHERE user_id = #{userId} AND type = 0",
|
||||
"AND source IN <foreach item='src' collection='sources' open='(' separator=',' close=')'>#{src}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
"UNION ALL",
|
||||
"SELECT amount FROM tt_user_blend_ercash_archive",
|
||||
"WHERE user_id = #{userId} AND type = 0",
|
||||
"AND source IN <foreach item='src' collection='sources' open='(' separator=',' close=')'>#{src}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
") t",
|
||||
"</script>"})
|
||||
BigDecimal getGameConsumeBySources(@Param("userId") Integer userId, @Param("sources") List<Integer> sources,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 按 source 列表统计用户群游戏消费(startTime/endTime 为空时为终身累计)
|
||||
*/
|
||||
@Select({"<script>",
|
||||
"SELECT COALESCE(SUM(ABS(t.amount)), 0) FROM (",
|
||||
"SELECT amount FROM tt_user_blend_ercash",
|
||||
"WHERE type = 0",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND source IN <foreach item='src' collection='sources' open='(' separator=',' close=')'>#{src}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
"UNION ALL",
|
||||
"SELECT amount FROM tt_user_blend_ercash_archive",
|
||||
"WHERE type = 0",
|
||||
"AND user_id IN <foreach item='id' collection='userIds' open='(' separator=',' close=')'>#{id}</foreach>",
|
||||
"AND source IN <foreach item='src' collection='sources' open='(' separator=',' close=')'>#{src}</foreach>",
|
||||
"<if test='startTime != null'>AND create_time >= #{startTime}</if>",
|
||||
"<if test='endTime != null'>AND create_time < #{endTime}</if>",
|
||||
") t",
|
||||
"</script>"})
|
||||
BigDecimal getGameConsumeByUserIds(@Param("userIds") List<Integer> userIds, @Param("sources") List<Integer> sources,
|
||||
@Param("startTime") Date startTime,
|
||||
@Param("endTime") Date endTime);
|
||||
|
||||
/**
|
||||
* 佣金明细记录
|
||||
*/
|
||||
List<Map<String, Object>> getCommissionDetailRecords(
|
||||
@Param("userIds") List<Integer> userIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商充值返利明细(实时计算口径):查询所有下级充值记录,按招商充值返点比例计算佣金。
|
||||
* JOIN tt_promotion_record 获取下级手机号。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessRechargeDetailRecords(
|
||||
@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商充值返利日报(实时计算口径):按 下级+日期 分组。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessRechargeDailyRecords(
|
||||
@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商充值返利汇总(实时计算口径):按 下级 分组。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessRechargeSummaryRecords(
|
||||
@Param("rechargeUserIds") List<Integer> rechargeUserIds,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商佣金明细(实时计算口径):按招商自身费率对整队普通用户每笔游戏消费计算逐笔明细。
|
||||
* 仅用于展示,发放仍由 CommissionSettlementTask 周结按 source 13 汇总入账。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessCommissionDetailRecords(
|
||||
@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商佣金日报(实时计算口径):按 下级+游戏+日期 分组,消费金额在 SQL 汇总,佣金在 Java 按招商费率计算。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessCommissionDailyRecords(
|
||||
@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 招商佣金汇总(实时计算口径):按 下级+游戏 分组,消费金额在 SQL 汇总,佣金在 Java 按招商费率计算。
|
||||
*/
|
||||
List<Map<String, Object>> getBusinessCommissionSummaryRecords(
|
||||
@Param("gameUserIds") List<Integer> gameUserIds,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 佣金日报
|
||||
*/
|
||||
List<Map<String, Object>> getCommissionDailyRecords(
|
||||
@Param("userIds") List<Integer> userIds,
|
||||
@Param("date") String date,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 佣金汇总
|
||||
*/
|
||||
List<Map<String, Object>> getCommissionSummaryRecords(
|
||||
@Param("userIds") List<Integer> userIds,
|
||||
@Param("startDate") String startDate,
|
||||
@Param("endDate") String endDate,
|
||||
@Param("subUserId") Integer subUserId);
|
||||
|
||||
/**
|
||||
* 下级充值报表
|
||||
*/
|
||||
List<Map<String, Object>> getSubordinateRechargeRecords(
|
||||
@Param("parentId") Integer parentId,
|
||||
@Param("month") String month,
|
||||
@Param("subUserId") Integer subUserId,
|
||||
@Param("phone") String phone);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.promo.model;
|
||||
|
||||
import cloud.tianai.captcha.validator.common.model.dto.ImageCaptchaTrack;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 推广后台滑块验证码校验请求
|
||||
*/
|
||||
@Data
|
||||
public class PromoCaptchaCheckRequest {
|
||||
|
||||
/**
|
||||
* 验证码 ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 滑块轨迹数据
|
||||
*/
|
||||
private ImageCaptchaTrack data;
|
||||
|
||||
/**
|
||||
* 手机号,用于绑定验证状态
|
||||
*/
|
||||
private String phoneNumber;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ruoyi.promo.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.promo.domain.entity.PromoCardKey;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 卡商卡密服务
|
||||
*/
|
||||
public interface PromoCardKeyService {
|
||||
|
||||
/**
|
||||
* 生成卡密
|
||||
* @param creatorId 卡商ID
|
||||
* @param creatorName 卡商名称
|
||||
* @param amount 卡密面值金额
|
||||
* @param num 生成数量
|
||||
* @return 生成的卡密列表
|
||||
*/
|
||||
R<List<String>> generateCardKeys(Long creatorId, String creatorName, BigDecimal amount, Integer num);
|
||||
|
||||
/**
|
||||
* 卡商销毁自己创建的未使用卡密:软删除(status=2)并按面值退回卡商余额(merchant_balance),写退款流水。
|
||||
* 幂等:仅当卡密仍为未使用(status=0)时可销毁,防止重复销毁/重复退款。
|
||||
* @param cardKeyId 卡密ID
|
||||
* @param operatorId 操作者(当前登录卡商)ID
|
||||
* @param isSuperAdmin 是否超级管理员(超管可销毁任意卡商的未使用卡密)
|
||||
* @return 销毁结果
|
||||
*/
|
||||
R<String> destroyCardKey(Long cardKeyId, Long operatorId, boolean isSuperAdmin);
|
||||
|
||||
/**
|
||||
* 获取卡商的卡密列表(分页)
|
||||
*/
|
||||
IPage<PromoCardKey> getCardKeyList(Long creatorId, String usedByName, String creatorName, Integer pageNum, Integer pageSize);
|
||||
|
||||
/**
|
||||
* 获取卡商信息(余额等)
|
||||
*/
|
||||
Map<String, Object> getMerchantInfo(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.ruoyi.promo.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkey;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 推广CDKEY服务
|
||||
*/
|
||||
public interface PromoCdkeyService {
|
||||
|
||||
/**
|
||||
* 生成CDKEY(仅主播账号可创建)
|
||||
* @param creatorId 创建者ID
|
||||
* @param creatorName 创建者名称
|
||||
* @param form 表单参数
|
||||
* @return 生成的CDKEY码列表
|
||||
*/
|
||||
R<List<String>> generateCdkeys(Long creatorId, String creatorName, Map<String, Object> form);
|
||||
|
||||
/**
|
||||
* 获取创建记录列表(当前登录主播自己创建的)
|
||||
* @param creatorId 创建者ID
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param status 状态(0=进行中,1=已结束)
|
||||
*/
|
||||
List<PromoCdkey> getCdkeyList(Long creatorId, Date startDate, Date endDate, Integer status);
|
||||
|
||||
/**
|
||||
* 获取兑换记录列表(当前主播创建的CDKEY被下级普通用户兑换的记录)
|
||||
* @param creatorId CDKEY创建者ID
|
||||
* @param startDate 开始日期
|
||||
* @param endDate 结束日期
|
||||
* @param redeemerName 兑换人名称
|
||||
* @param cdkeyCode CDKEY码
|
||||
*/
|
||||
List<PromoCdkeyRedeem> getRedeemList(Long creatorId, Date startDate, Date endDate,
|
||||
String redeemerName, String cdkeyCode);
|
||||
|
||||
/**
|
||||
* 创建日报
|
||||
*/
|
||||
List<Map<String, Object>> getCreateDailyReport(Long creatorId, Date startDate, Date endDate);
|
||||
|
||||
/**
|
||||
* 兑换日报
|
||||
*/
|
||||
List<Map<String, Object>> getRedeemDailyReport(Long creatorId, Date startDate, Date endDate);
|
||||
|
||||
/**
|
||||
* 全局补齐已过期状态(定时任务使用)
|
||||
* @return 受影响行数
|
||||
*/
|
||||
int markExpiredCdkeys();
|
||||
|
||||
/**
|
||||
* 查询待退还的CDK:已结束(status=1)且未退还(refund_time IS NULL)
|
||||
*/
|
||||
List<PromoCdkey> selectRefundableCdkeys();
|
||||
|
||||
/**
|
||||
* 对单个CDK执行精确退还(预扣总额 - 实际派发),金币与积分同额。
|
||||
* 幂等:重复调用或已退还时返回 0。
|
||||
* @return 本次实际退还金额
|
||||
*/
|
||||
BigDecimal refundOne(PromoCdkey cdkey);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ruoyi.promo.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.domain.entity.TtRechargeProd;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.contract.request.RegisterRequest;
|
||||
import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest;
|
||||
import com.ruoyi.promo.contract.response.GetShareDomainResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PromoLoginService {
|
||||
|
||||
String login(String username, String password);
|
||||
|
||||
/**
|
||||
* 推广后台短信验证码登录;手机号未注册时,带上级邀请码则自动注册为主播
|
||||
*/
|
||||
String smsLogin(String phoneNumber, String code, String parentInvitationCode);
|
||||
|
||||
String createUserToken(String phoneNumber);
|
||||
|
||||
R<TtUser> register(RegisterRequest request);
|
||||
|
||||
R<TtUser> updateInvitationCode(UpdateInvitationCodeRequest request);
|
||||
|
||||
R<GetShareDomainResponse> getShareDomain();
|
||||
|
||||
List<TtRechargeProd> queryList();
|
||||
|
||||
List<String> generateCard(Integer userId, Integer rechargeListId, Integer num);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ruoyi.promo.service;
|
||||
|
||||
import com.ruoyi.promo.domain.entity.PromoMenuConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 推广后台菜单权限服务(按角色类型分配)
|
||||
*/
|
||||
public interface PromoMenuService {
|
||||
|
||||
/**
|
||||
* 根据用户类型获取菜单路径
|
||||
* 超级管理员返回所有菜单
|
||||
*/
|
||||
List<String> getMenuPathsByUserType(String userType);
|
||||
|
||||
/**
|
||||
* 超管:保存某角色类型的菜单权限
|
||||
*/
|
||||
void saveRoleMenus(String roleType, List<String> menuPaths);
|
||||
|
||||
/**
|
||||
* 获取某角色类型已配置的菜单路径
|
||||
*/
|
||||
List<String> getRoleMenuPaths(String roleType);
|
||||
|
||||
/**
|
||||
* 获取所有可分配的菜单配置(从数据库读取)
|
||||
*/
|
||||
List<PromoMenuConfig> getAllMenuConfigs();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.ruoyi.promo.service;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.domain.entity.TtCommissionRecord;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.contract.request.GetPurchaseByUserIdRequest;
|
||||
import com.ruoyi.promo.contract.request.GetSubBranchesRequest;
|
||||
import com.ruoyi.promo.contract.request.QueryPromoteWelfareRequest;
|
||||
import com.ruoyi.promo.contract.response.GetSubBranchesResponse;
|
||||
import com.ruoyi.promo.contract.response.QueryPromotionWelfareResponse;
|
||||
import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO;
|
||||
import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public interface PromoTurnoverService {
|
||||
|
||||
/**
|
||||
* 递归全部最下级玩家
|
||||
*/
|
||||
List<TtUser> recursionAllPlayer(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取实时数据
|
||||
*/
|
||||
AjaxResult getRealTimeData(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取首页游戏返利看板数据(今日/昨日/本周/累计,按游戏类型拆分)
|
||||
*/
|
||||
AjaxResult getGameCommissionData(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取近10天推广数据
|
||||
*/
|
||||
AjaxResult getLast10DaysPromotionData(Integer userId);
|
||||
|
||||
/**
|
||||
* 获取名下主播每日流水
|
||||
*/
|
||||
List<AnchorDayTurnoverVO> getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO);
|
||||
|
||||
/**
|
||||
* 获取下级分支
|
||||
*/
|
||||
GetSubBranchesResponse getSubBranches(GetSubBranchesRequest request);
|
||||
|
||||
/**
|
||||
* 获取日流水记录
|
||||
*/
|
||||
TableDataInfo getPurchaseByUserId(GetPurchaseByUserIdRequest userId);
|
||||
|
||||
/**
|
||||
* 获取佣金比例
|
||||
*/
|
||||
BigDecimal getCommissionRateByUserId(Integer userId);
|
||||
|
||||
/**
|
||||
* 修改佣金比例
|
||||
*/
|
||||
int updateCommissionRate(Integer userId, BigDecimal commissionRate);
|
||||
|
||||
/**
|
||||
* 获取佣金列表
|
||||
*/
|
||||
List<TtCommissionRecord> getCommissionList(Integer userId);
|
||||
|
||||
QueryPromotionWelfareResponse queryPromoteWelfare(Integer userId, QueryPromoteWelfareRequest request);
|
||||
|
||||
/**
|
||||
* 递归获取所有下级用户ID(包含自身,招商→主播+普通用户)
|
||||
*/
|
||||
void getAllSubIdsRecursive(Integer parentId, List<Integer> subUserIds);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.ruoyi.promo.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.admin.mapper.TtRechargeProdMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordType;
|
||||
import com.ruoyi.domain.entity.TtRechargeProd;
|
||||
import com.ruoyi.domain.entity.TtUserBlendErcash;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.domain.entity.PromoCardKey;
|
||||
import com.ruoyi.promo.mapper.PromoCardKeyMapper;
|
||||
import com.ruoyi.promo.service.PromoCardKeyService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class PromoCardKeyServiceImpl implements PromoCardKeyService {
|
||||
|
||||
@Autowired
|
||||
private PromoCardKeyMapper promoCardKeyMapper;
|
||||
|
||||
@Autowired
|
||||
private TtRechargeProdMapper rechargeProdMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper ttUserMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserBlendErcashMapper blendErcashMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public R<List<String>> generateCardKeys(Long creatorId, String creatorName, BigDecimal amount, Integer num) {
|
||||
if (num == null || num <= 0 || num > 100) {
|
||||
return R.fail("生成数量必须在1-100之间");
|
||||
}
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return R.fail("金额必须大于0");
|
||||
}
|
||||
|
||||
BigDecimal unitPrice = amount;
|
||||
BigDecimal totalCost = unitPrice.multiply(BigDecimal.valueOf(num));
|
||||
|
||||
// 扣除卡商专用余额(merchant_balance)
|
||||
int rows = ttUserMapper.subtractMerchantBalance(creatorId, totalCost);
|
||||
if (rows <= 0) {
|
||||
return R.fail("卡商余额不足,需要" + totalCost.toPlainString() + "卡商余额");
|
||||
}
|
||||
|
||||
// 查询扣减后余额并记录流水
|
||||
TtUser afterUser = ttUserMapper.selectById(creatorId);
|
||||
BigDecimal newBalance = afterUser.getMerchantBalance() != null ? afterUser.getMerchantBalance() : BigDecimal.ZERO;
|
||||
TtUserBlendErcash ercash = TtUserBlendErcash.builder()
|
||||
.userId(creatorId.intValue())
|
||||
.amount(BigDecimal.ZERO)
|
||||
.finalAmount(afterUser.getAccountAmount())
|
||||
.credits(totalCost.negate())
|
||||
.finalCredits(newBalance)
|
||||
.total(totalCost.negate())
|
||||
.type(TtAccountRecordType.OUTPUT.getCode())
|
||||
.source(TtAccountRecordSource.CARD_MERCHANT_GENERATE.getCode())
|
||||
.remark(TtAccountRecordSource.CARD_MERCHANT_GENERATE.getMsg() + "(" + num + "张)")
|
||||
.createTime(new Timestamp(System.currentTimeMillis()))
|
||||
.updateTime(new Timestamp(System.currentTimeMillis()))
|
||||
.build();
|
||||
blendErcashMapper.insert(ercash);
|
||||
|
||||
// 批量生成卡密
|
||||
List<String> cardCodes = new ArrayList<>();
|
||||
for (int i = 0; i < num; i++) {
|
||||
String code = IdUtils.fastSimpleUUID().substring(0, 16).toUpperCase();
|
||||
PromoCardKey cardKey = PromoCardKey.builder()
|
||||
.cardCode(code)
|
||||
.amount(unitPrice)
|
||||
.creatorId(creatorId)
|
||||
.creatorName(creatorName)
|
||||
.status(0)
|
||||
.createTime(DateUtils.getNowDate())
|
||||
.build();
|
||||
promoCardKeyMapper.insert(cardKey);
|
||||
cardCodes.add(code);
|
||||
}
|
||||
|
||||
return R.ok(cardCodes);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public R<String> destroyCardKey(Long cardKeyId, Long operatorId, boolean isSuperAdmin) {
|
||||
if (cardKeyId == null) {
|
||||
return R.fail("参数不完整");
|
||||
}
|
||||
PromoCardKey cardKey = promoCardKeyMapper.selectById(cardKeyId);
|
||||
if (cardKey == null) {
|
||||
return R.fail("卡密不存在");
|
||||
}
|
||||
// 权限:卡商仅能销毁自己创建的卡密,超管可销毁任意
|
||||
if (!isSuperAdmin && (cardKey.getCreatorId() == null || !cardKey.getCreatorId().equals(operatorId))) {
|
||||
return R.fail("无权销毁他人创建的卡密");
|
||||
}
|
||||
// 前置友好校验:已使用/已销毁不可再销毁
|
||||
Integer status = cardKey.getStatus();
|
||||
if (status != null && status == 1) {
|
||||
return R.fail("该卡密已被使用,不能销毁");
|
||||
}
|
||||
if (status != null && status == 2) {
|
||||
return R.fail("该卡密已销毁,请勿重复操作");
|
||||
}
|
||||
|
||||
// 原子幂等:仅当仍为未使用(status=0)时置为已销毁(status=2),防并发兑换/重复销毁导致重复退款
|
||||
LambdaUpdateWrapper<PromoCardKey> uw = new LambdaUpdateWrapper<>();
|
||||
uw.eq(PromoCardKey::getId, cardKeyId)
|
||||
.eq(PromoCardKey::getStatus, 0)
|
||||
.set(PromoCardKey::getStatus, 2);
|
||||
int marked = promoCardKeyMapper.update(null, uw);
|
||||
if (marked <= 0) {
|
||||
// 期间被兑换或被其他请求销毁
|
||||
return R.fail("卡密状态已变更(可能已被使用或已销毁),操作未生效");
|
||||
}
|
||||
|
||||
// 按面值全额退回卡商余额,退给卡密创建者
|
||||
BigDecimal refund = cardKey.getAmount() != null ? cardKey.getAmount() : BigDecimal.ZERO;
|
||||
Long creatorId = cardKey.getCreatorId();
|
||||
if (refund.compareTo(BigDecimal.ZERO) > 0 && creatorId != null) {
|
||||
ttUserMapper.addMerchantBalance(creatorId, refund);
|
||||
|
||||
// 写退款流水(镜像生成时的卡商余额记账约定:credits 携带卡商余额变动,finalCredits 为变动后余额)
|
||||
TtUser afterUser = ttUserMapper.selectById(creatorId);
|
||||
BigDecimal newBalance = (afterUser != null && afterUser.getMerchantBalance() != null)
|
||||
? afterUser.getMerchantBalance() : BigDecimal.ZERO;
|
||||
BigDecimal afterAccountAmount = (afterUser != null && afterUser.getAccountAmount() != null)
|
||||
? afterUser.getAccountAmount() : BigDecimal.ZERO;
|
||||
Timestamp now = new Timestamp(System.currentTimeMillis());
|
||||
TtUserBlendErcash ercash = TtUserBlendErcash.builder()
|
||||
.userId(creatorId.intValue())
|
||||
.amount(BigDecimal.ZERO)
|
||||
.finalAmount(afterAccountAmount)
|
||||
.credits(refund)
|
||||
.finalCredits(newBalance)
|
||||
.total(refund)
|
||||
.type(TtAccountRecordType.INPUT.getCode())
|
||||
.source(TtAccountRecordSource.CARD_MERCHANT_DESTROY_REFUND.getCode())
|
||||
.remark(TtAccountRecordSource.CARD_MERCHANT_DESTROY_REFUND.getMsg() + "(" + cardKey.getCardCode() + ")")
|
||||
.createTime(now)
|
||||
.updateTime(now)
|
||||
.build();
|
||||
blendErcashMapper.insert(ercash);
|
||||
}
|
||||
|
||||
return R.ok("销毁成功,已退回卡商余额 " + refund.toPlainString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPage<PromoCardKey> getCardKeyList(Long creatorId, String usedByName, String creatorName, Integer pageNum, Integer pageSize) {
|
||||
LambdaQueryWrapper<PromoCardKey> wrapper = new LambdaQueryWrapper<>();
|
||||
if (creatorId != null) {
|
||||
wrapper.eq(PromoCardKey::getCreatorId, creatorId);
|
||||
}
|
||||
if (StringUtils.isNotBlank(usedByName)) {
|
||||
wrapper.like(PromoCardKey::getUsedByName, usedByName);
|
||||
}
|
||||
if (StringUtils.isNotBlank(creatorName)) {
|
||||
wrapper.like(PromoCardKey::getCreatorName, creatorName);
|
||||
}
|
||||
// 按创建时间倒序,相同时间再按 id 倒序,保证同一批次生成的卡密顺序稳定
|
||||
wrapper.orderByDesc(PromoCardKey::getCreateTime).orderByDesc(PromoCardKey::getId);
|
||||
Page<PromoCardKey> page = new Page<>(
|
||||
pageNum == null || pageNum < 1 ? 1 : pageNum,
|
||||
pageSize == null || pageSize < 1 ? 10 : pageSize);
|
||||
return promoCardKeyMapper.selectPage(page, wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getMerchantInfo(Long userId) {
|
||||
TtUser user = ttUserMapper.selectById(userId);
|
||||
Map<String, Object> info = new LinkedHashMap<>();
|
||||
if (user != null) {
|
||||
info.put("userName", user.getUserName());
|
||||
info.put("nickName", user.getNickName());
|
||||
info.put("balance", user.getMerchantBalance() != null ? user.getMerchantBalance() : BigDecimal.ZERO);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package com.ruoyi.promo.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordType;
|
||||
import com.ruoyi.domain.entity.TtUserBlendErcash;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkey;
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkeyRedeem;
|
||||
import com.ruoyi.promo.mapper.PromoCdkeyMapper;
|
||||
import com.ruoyi.promo.mapper.PromoCdkeyRedeemMapper;
|
||||
import com.ruoyi.promo.service.PromoCdkeyService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.sql.Timestamp;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 推广CDKEY服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PromoCdkeyServiceImpl implements PromoCdkeyService {
|
||||
|
||||
@Autowired
|
||||
private PromoCdkeyMapper promoCdkeyMapper;
|
||||
|
||||
@Autowired
|
||||
private PromoCdkeyRedeemMapper promoCdkeyRedeemMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper ttUserMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserBlendErcashMapper blendErcashMapper;
|
||||
|
||||
/**
|
||||
* 生成CDKEY
|
||||
* 表单参数:
|
||||
* title - CDKEY标题
|
||||
* amount1 - 最低金额(金币)
|
||||
* amount2 - 最高金额(金币)
|
||||
* num - 每个码可用次数
|
||||
* description - 描述
|
||||
* openingTime - 开启时间
|
||||
* validity - 有效期
|
||||
* createNum - 生成码的数量
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public R<List<String>> generateCdkeys(Long creatorId, String creatorName, Map<String, Object> form) {
|
||||
String title = (String) form.get("title");
|
||||
if (title == null || title.trim().isEmpty()) {
|
||||
return R.fail("CDKEY标题不能为空");
|
||||
}
|
||||
|
||||
// 解析金额
|
||||
BigDecimal amount1 = parseBigDecimal(form.get("amount1"));
|
||||
BigDecimal amount2 = parseBigDecimal(form.get("amount2"));
|
||||
if (amount1 == null || amount1.compareTo(BigDecimal.ZERO) < 0) {
|
||||
return R.fail("最低金额不合法");
|
||||
}
|
||||
if (amount2 == null || amount2.compareTo(amount1) < 0) {
|
||||
return R.fail("最高金额不能小于最低金额");
|
||||
}
|
||||
|
||||
// 解析可用次数(设置上限,防止超大参数导致天量预扣计算/异常数据)
|
||||
Integer num = parseInteger(form.get("num"));
|
||||
if (num == null || num <= 0 || num > 1000) {
|
||||
return R.fail("CDKEY个数必须在1-1000之间");
|
||||
}
|
||||
|
||||
// 解析生成数量
|
||||
Integer createNum = parseInteger(form.get("createNum"));
|
||||
if (createNum == null || createNum <= 0 || createNum > 100) {
|
||||
return R.fail("生成数量必须在1-100之间");
|
||||
}
|
||||
|
||||
String description = (String) form.get("description");
|
||||
|
||||
// 解析时间
|
||||
Date openingTime = parseDate(form.get("openingTime"));
|
||||
if (openingTime == null) {
|
||||
return R.fail("开启时间不能为空");
|
||||
}
|
||||
Date validity = parseDate(form.get("validity"));
|
||||
|
||||
// 计算总扣款金额 = 最高金额 × 每个码可用次数 × 生成数量(按最大可能派奖金额预扣)
|
||||
BigDecimal totalCost = amount2.multiply(BigDecimal.valueOf(num)).multiply(BigDecimal.valueOf(createNum));
|
||||
|
||||
// 校验主播余额是否充足
|
||||
TtUser anchorUser = ttUserMapper.selectById(creatorId);
|
||||
if (anchorUser == null) {
|
||||
return R.fail("主播账号不存在");
|
||||
}
|
||||
BigDecimal currentBalance = anchorUser.getAccountAmount() != null ? anchorUser.getAccountAmount() : BigDecimal.ZERO;
|
||||
BigDecimal currentCredits = anchorUser.getAccountCredits() != null ? anchorUser.getAccountCredits() : BigDecimal.ZERO;
|
||||
if (currentBalance.compareTo(totalCost) < 0) {
|
||||
return R.fail("余额不足,需要 " + totalCost.toPlainString() + " 金币,当前余额 " + currentBalance.toPlainString());
|
||||
}
|
||||
if (currentCredits.compareTo(totalCost) < 0) {
|
||||
return R.fail("积分不足,需要 " + totalCost.toPlainString() + " 积分,当前积分 " + currentCredits.toPlainString());
|
||||
}
|
||||
|
||||
// 扣减主播余额(金币)
|
||||
int rows = ttUserMapper.subtractOnlyAccountAmount(creatorId, totalCost);
|
||||
if (rows <= 0) {
|
||||
return R.fail("余额扣减失败,余额不足");
|
||||
}
|
||||
// 扣减主播积分
|
||||
rows = ttUserMapper.subtractOnlyAccountCredits(creatorId, totalCost);
|
||||
if (rows <= 0) {
|
||||
// 积分不足时事务回滚
|
||||
throw new RuntimeException("积分扣减失败,积分不足");
|
||||
}
|
||||
|
||||
// 查询扣减后余额,写入流水
|
||||
TtUser afterUser = ttUserMapper.selectById(creatorId);
|
||||
BigDecimal newBalance = afterUser.getAccountAmount() != null ? afterUser.getAccountAmount() : BigDecimal.ZERO;
|
||||
BigDecimal newCredits = afterUser.getAccountCredits() != null ? afterUser.getAccountCredits() : BigDecimal.ZERO;
|
||||
Timestamp now = new Timestamp(System.currentTimeMillis());
|
||||
TtUserBlendErcash ercash = TtUserBlendErcash.builder()
|
||||
.userId(afterUser.getUserId())
|
||||
.amount(totalCost.negate())
|
||||
.finalAmount(newBalance)
|
||||
.credits(totalCost.negate())
|
||||
.finalCredits(newCredits)
|
||||
.total(totalCost.negate())
|
||||
.type(TtAccountRecordType.OUTPUT.getCode())
|
||||
.source(TtAccountRecordSource.PROMO_CDKEY_GENERATE.getCode())
|
||||
.remark(TtAccountRecordSource.PROMO_CDKEY_GENERATE.getMsg() + "(" + createNum + "个码×" + num + "次)")
|
||||
.createTime(now)
|
||||
.updateTime(now)
|
||||
.build();
|
||||
blendErcashMapper.insert(ercash);
|
||||
|
||||
// 批量生成CDKEY
|
||||
List<String> cdkeyCodes = new ArrayList<>();
|
||||
for (int i = 0; i < createNum; i++) {
|
||||
String code = generateCdkeyCode();
|
||||
|
||||
PromoCdkey cdkey = PromoCdkey.builder()
|
||||
.title(title.trim())
|
||||
.cdkeyCode(code)
|
||||
.amountMin(amount1)
|
||||
.amountMax(amount2)
|
||||
.num(num)
|
||||
.usableCount(num)
|
||||
.usedCount(0)
|
||||
.description(description)
|
||||
.openingTime(openingTime)
|
||||
.validity(validity)
|
||||
.status(0) // 进行中
|
||||
.creatorId(creatorId)
|
||||
.creatorName(creatorName)
|
||||
.createTime(DateUtils.getNowDate())
|
||||
.build();
|
||||
promoCdkeyMapper.insert(cdkey);
|
||||
cdkeyCodes.add(code);
|
||||
}
|
||||
|
||||
return R.ok(cdkeyCodes, "生成成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PromoCdkey> getCdkeyList(Long creatorId, Date startDate, Date endDate, Integer status) {
|
||||
// 自动将已过期的CDKEY状态更新为已结束
|
||||
LambdaQueryWrapper<PromoCdkey> expireWrapper = new LambdaQueryWrapper<>();
|
||||
expireWrapper.eq(PromoCdkey::getCreatorId, creatorId)
|
||||
.eq(PromoCdkey::getStatus, 0)
|
||||
.le(PromoCdkey::getValidity, new Date());
|
||||
List<PromoCdkey> expiredList = promoCdkeyMapper.selectList(expireWrapper);
|
||||
if (!expiredList.isEmpty()) {
|
||||
List<Long> expiredIds = new ArrayList<>();
|
||||
for (PromoCdkey c : expiredList) {
|
||||
expiredIds.add(c.getId());
|
||||
}
|
||||
PromoCdkey updateEntity = new PromoCdkey();
|
||||
updateEntity.setStatus(1);
|
||||
LambdaQueryWrapper<PromoCdkey> updateWrapper = new LambdaQueryWrapper<>();
|
||||
updateWrapper.in(PromoCdkey::getId, expiredIds);
|
||||
promoCdkeyMapper.update(updateEntity, updateWrapper);
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<PromoCdkey> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PromoCdkey::getCreatorId, creatorId);
|
||||
if (startDate != null) {
|
||||
wrapper.ge(PromoCdkey::getCreateTime, startDate);
|
||||
}
|
||||
if (endDate != null) {
|
||||
wrapper.lt(PromoCdkey::getCreateTime, endDate);
|
||||
}
|
||||
if (status != null) {
|
||||
if (status == 2) {
|
||||
// “已过期”筛选: status=1 且 validity 已过
|
||||
wrapper.eq(PromoCdkey::getStatus, 1)
|
||||
.le(PromoCdkey::getValidity, new Date());
|
||||
} else {
|
||||
wrapper.eq(PromoCdkey::getStatus, status);
|
||||
}
|
||||
}
|
||||
wrapper.orderByDesc(PromoCdkey::getCreateTime);
|
||||
return promoCdkeyMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PromoCdkeyRedeem> getRedeemList(Long creatorId, Date startDate, Date endDate,
|
||||
String redeemerName, String cdkeyCode) {
|
||||
// 先查出当前主播创建的所有CDKEY id
|
||||
LambdaQueryWrapper<PromoCdkey> cdkeyWrapper = new LambdaQueryWrapper<>();
|
||||
cdkeyWrapper.eq(PromoCdkey::getCreatorId, creatorId);
|
||||
List<PromoCdkey> cdkeys = promoCdkeyMapper.selectList(cdkeyWrapper);
|
||||
if (cdkeys.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Long> cdkeyIds = new ArrayList<>();
|
||||
for (PromoCdkey c : cdkeys) {
|
||||
cdkeyIds.add(c.getId());
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<PromoCdkeyRedeem> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.in(PromoCdkeyRedeem::getCdkeyId, cdkeyIds);
|
||||
if (startDate != null) {
|
||||
wrapper.ge(PromoCdkeyRedeem::getRedeemTime, startDate);
|
||||
}
|
||||
if (endDate != null) {
|
||||
wrapper.lt(PromoCdkeyRedeem::getRedeemTime, endDate);
|
||||
}
|
||||
if (redeemerName != null && !redeemerName.trim().isEmpty()) {
|
||||
wrapper.like(PromoCdkeyRedeem::getRedeemerName, redeemerName.trim());
|
||||
}
|
||||
if (cdkeyCode != null && !cdkeyCode.trim().isEmpty()) {
|
||||
wrapper.like(PromoCdkeyRedeem::getCdkeyCode, cdkeyCode.trim());
|
||||
}
|
||||
wrapper.orderByDesc(PromoCdkeyRedeem::getRedeemTime);
|
||||
return promoCdkeyRedeemMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getCreateDailyReport(Long creatorId, Date startDate, Date endDate) {
|
||||
return promoCdkeyMapper.getCreateDailyReport(creatorId, startDate, endDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getRedeemDailyReport(Long creatorId, Date startDate, Date endDate) {
|
||||
return promoCdkeyRedeemMapper.getRedeemDailyReport(creatorId, startDate, endDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int markExpiredCdkeys() {
|
||||
return promoCdkeyMapper.markExpired();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PromoCdkey> selectRefundableCdkeys() {
|
||||
LambdaQueryWrapper<PromoCdkey> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(PromoCdkey::getStatus, 1)
|
||||
.isNull(PromoCdkey::getRefundTime);
|
||||
return promoCdkeyMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public BigDecimal refundOne(PromoCdkey cdkey) {
|
||||
Long id = cdkey.getId();
|
||||
// 精确退还 = 预扣总额(amountMax×num) - 实际已派发金币总额
|
||||
BigDecimal amountMax = cdkey.getAmountMax() != null ? cdkey.getAmountMax() : BigDecimal.ZERO;
|
||||
int num = cdkey.getNum() != null ? cdkey.getNum() : 0;
|
||||
BigDecimal prepaid = amountMax.multiply(BigDecimal.valueOf(num));
|
||||
BigDecimal distributed = promoCdkeyRedeemMapper.sumRedeemedAmount(id);
|
||||
if (distributed == null) {
|
||||
distributed = BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal refund = prepaid.subtract(distributed);
|
||||
if (refund.compareTo(BigDecimal.ZERO) < 0) {
|
||||
refund = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 幂等抢占:仅当 refund_time 为空时标记成功,防并发/重复退还
|
||||
Date now = new Date();
|
||||
int marked = promoCdkeyMapper.markRefunded(id, refund, now);
|
||||
if (marked <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (refund.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 回补主播金币与积分(同额,与创建时双向预扣对应)
|
||||
Long creatorId = cdkey.getCreatorId();
|
||||
ttUserMapper.addOnlyAccountAmount(creatorId, refund);
|
||||
ttUserMapper.addOnlyAccountCredits(creatorId, refund);
|
||||
|
||||
// 写入退还流水
|
||||
TtUser afterUser = ttUserMapper.selectById(creatorId);
|
||||
if (afterUser == null) {
|
||||
log.warn("[CDK结束退还] creatorId={} 对应的用户不存在(可能已被删除),本次退还中止", creatorId);
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal newBalance = afterUser.getAccountAmount() != null ? afterUser.getAccountAmount() : BigDecimal.ZERO;
|
||||
BigDecimal newCredits = afterUser.getAccountCredits() != null ? afterUser.getAccountCredits() : BigDecimal.ZERO;
|
||||
Timestamp ts = new Timestamp(System.currentTimeMillis());
|
||||
TtUserBlendErcash ercash = TtUserBlendErcash.builder()
|
||||
.userId(afterUser.getUserId())
|
||||
.amount(refund)
|
||||
.finalAmount(newBalance)
|
||||
.credits(refund)
|
||||
.finalCredits(newCredits)
|
||||
.total(refund)
|
||||
.type(TtAccountRecordType.INPUT.getCode())
|
||||
.source(TtAccountRecordSource.PROMO_CDKEY_REFUND.getCode())
|
||||
.remark(TtAccountRecordSource.PROMO_CDKEY_REFUND.getMsg() + "(" + cdkey.getCdkeyCode() + ")")
|
||||
.createTime(ts)
|
||||
.updateTime(ts)
|
||||
.build();
|
||||
blendErcashMapper.insert(ercash);
|
||||
|
||||
return refund;
|
||||
}
|
||||
|
||||
// ================= 工具方法 =================
|
||||
|
||||
/**
|
||||
* 生成16位CDKEY码,格式: XXXX-XXXX-XXXX-XXXX
|
||||
*/
|
||||
private String generateCdkeyCode() {
|
||||
String raw = IdUtils.fastSimpleUUID().substring(0, 16).toUpperCase();
|
||||
return raw.substring(0, 4) + "-" + raw.substring(4, 8) + "-"
|
||||
+ raw.substring(8, 12) + "-" + raw.substring(12, 16);
|
||||
}
|
||||
|
||||
private BigDecimal parseBigDecimal(Object val) {
|
||||
if (val == null) return null;
|
||||
try {
|
||||
if (val instanceof Number) {
|
||||
return BigDecimal.valueOf(((Number) val).doubleValue()).setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
String s = val.toString().trim();
|
||||
if (s.isEmpty()) return null;
|
||||
return new BigDecimal(s).setScale(2, RoundingMode.HALF_UP);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer parseInteger(Object val) {
|
||||
if (val == null) return null;
|
||||
if (val instanceof Number) {
|
||||
return ((Number) val).intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(val.toString().trim());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Date parseDate(Object val) {
|
||||
if (val == null) return null;
|
||||
if (val instanceof Date) return (Date) val;
|
||||
String s = val.toString().trim();
|
||||
if (s.isEmpty()) return null;
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.parse(s);
|
||||
} catch (ParseException e) {
|
||||
try {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
return sdf.parse(s);
|
||||
} catch (ParseException e2) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package com.ruoyi.promo.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
|
||||
import com.ruoyi.admin.mapper.PromoAdminMapper;
|
||||
import com.ruoyi.admin.mapper.TtRechargeCardMapper;
|
||||
import com.ruoyi.admin.mapper.TtRechargeProdMapper;
|
||||
import com.ruoyi.admin.mapper.TtRedPacketMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.admin.service.TtUserAvatarService;
|
||||
import com.ruoyi.admin.service.TtUserService;
|
||||
import com.ruoyi.admin.util.RandomUtils;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.RandomUtil;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.ip.IpUtils;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.common.utils.uuid.UUID;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.domain.entity.TtRechargeProd;
|
||||
import com.ruoyi.domain.entity.sys.PromoAdmin;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.domain.other.TtRedPacket;
|
||||
import com.ruoyi.domain.other.TtUserAvatar;
|
||||
import com.ruoyi.framework.web.service.SysLoginService;
|
||||
import com.ruoyi.framework.web.service.TokenService;
|
||||
import com.ruoyi.promo.contract.request.RegisterRequest;
|
||||
import com.ruoyi.promo.contract.request.UpdateInvitationCodeRequest;
|
||||
import com.ruoyi.promo.contract.response.GetShareDomainResponse;
|
||||
import com.ruoyi.promo.service.PromoLoginService;
|
||||
import com.ruoyi.system.domain.SysConfig;
|
||||
import com.ruoyi.system.mapper.SysConfigMapper;
|
||||
import com.ruoyi.thirdparty.common.service.ApiSmsService;
|
||||
import com.ruoyi.admin.security.ApiUserDetailsServiceImpl;
|
||||
import com.ruoyi.admin.service.UserIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class PromoLoginServiceImpl implements PromoLoginService {
|
||||
|
||||
|
||||
@Autowired
|
||||
private SysLoginService sysLoginService;
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private ApiUserDetailsServiceImpl apiUserDetailsService;
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper ttUserMapper;
|
||||
|
||||
@Autowired
|
||||
private PromoAdminMapper promoAdminMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserService userService;
|
||||
|
||||
@Autowired
|
||||
private TtUserAvatarService userAvatarService;
|
||||
|
||||
@Autowired
|
||||
private ApiSmsService apiSmsService;
|
||||
|
||||
@Autowired
|
||||
private UserIdGenerator userIdGenerator;
|
||||
|
||||
@Autowired
|
||||
private SysConfigMapper sysConfigMapper;
|
||||
|
||||
@Autowired
|
||||
private TtRechargeCardMapper rechargeCardMapper;
|
||||
|
||||
@Autowired
|
||||
private TtRechargeProdMapper rechargeListMapper;
|
||||
|
||||
@Autowired
|
||||
private TtRedPacketMapper redPacketMapper;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/** 推广码已修改标记 Redis Key 前缀 */
|
||||
private static final String INVITATION_CODE_MODIFIED_KEY = "promo:invitation_code_modified:";
|
||||
|
||||
|
||||
/**
|
||||
* 推广后台账号密码登录(短信未接入前统一走这里):
|
||||
* ① 超管 → promo_admin(username 或 phone_number)
|
||||
* ② 招商(04)/主播(01)/卡商(05) → tt_user(账号为手机号)
|
||||
*/
|
||||
@Override
|
||||
public String login(String username, String password) {
|
||||
sysLoginService.loginPreCheck(username, password);
|
||||
String account = username.trim();
|
||||
|
||||
// ① 超级管理员
|
||||
PromoAdmin promoAdmin = promoAdminMapper.findByAccount(account);
|
||||
if (promoAdmin != null) {
|
||||
if (!SecurityUtils.matchesPassword(password, promoAdmin.getPassword())) {
|
||||
throw new IllegalArgumentException("账号或密码错误");
|
||||
}
|
||||
// 走 promo_ 前缀,确保拿到的是超管身份(promo_admin 表)
|
||||
LoginUser loginUser = sysLoginService.createLoginUser("promo_" + promoAdmin.getUsername(), password);
|
||||
return tokenService.createToken(loginUser);
|
||||
}
|
||||
|
||||
// ② 招商/主播/卡商
|
||||
TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.and(w -> w.eq(TtUser::getPhoneNumber, account).or().eq(TtUser::getUserName, account))
|
||||
.one();
|
||||
if (user == null) {
|
||||
throw new IllegalArgumentException("账号或密码错误");
|
||||
}
|
||||
if (!UserType.ANCHOR.getCode().equals(user.getUserType())
|
||||
&& !UserType.BUSINESS_USER.getCode().equals(user.getUserType())
|
||||
&& !UserType.CARD_MERCHANT.getCode().equals(user.getUserType())) {
|
||||
throw new IllegalArgumentException("该账号无推广后台登录权限");
|
||||
}
|
||||
if (!SecurityUtils.matchesPassword(password, user.getPassword())) {
|
||||
throw new IllegalArgumentException("账号或密码错误");
|
||||
}
|
||||
return createUserToken(user.getPhoneNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个框既能填 4 位短信验证码,也能直接填密码:
|
||||
* ① 非 4 位数字 → 按密码处理(超管 promo_admin / 招商·主播·卡商 tt_user)
|
||||
* ② 4 位数字 → 按短信验证码处理
|
||||
*/
|
||||
@Override
|
||||
public String smsLogin(String phoneNumber, String code, String parentInvitationCode) {
|
||||
String account = StringUtils.trim(phoneNumber);
|
||||
String value = StringUtils.trim(code);
|
||||
|
||||
PromoAdmin promoAdmin = StringUtils.isNotEmpty(account) ? promoAdminMapper.findByAccount(account) : null;
|
||||
TtUser ttUser = StringUtils.isNotEmpty(account) ? findUserByAccount(account) : null;
|
||||
boolean hasPromoRole = ttUser != null
|
||||
&& !"1".equals(ttUser.getDelFlag())
|
||||
&& !"1".equals(ttUser.getStatus())
|
||||
&& (UserType.ANCHOR.getCode().equals(ttUser.getUserType())
|
||||
|| UserType.BUSINESS_USER.getCode().equals(ttUser.getUserType())
|
||||
|| UserType.CARD_MERCHANT.getCode().equals(ttUser.getUserType()));
|
||||
if (promoAdmin == null && ttUser == null && !Validator.isMobile(account)) {
|
||||
throw new IllegalArgumentException("手机号格式错误,请检查手机号是否输入正确!");
|
||||
}
|
||||
boolean isCode = NumberUtil.isNumber(value) && value.length() == 4;
|
||||
|
||||
// ① 密码登录
|
||||
if (!isCode && StringUtils.isNotEmpty(value)) {
|
||||
if (promoAdmin != null && StringUtils.isNotEmpty(promoAdmin.getPassword())
|
||||
&& SecurityUtils.matchesPassword(value, promoAdmin.getPassword())) {
|
||||
// 走 promo_ 前缀,确保拿到的是超管身份
|
||||
LoginUser loginUser = sysLoginService.createLoginUser("promo_" + promoAdmin.getUsername(), value);
|
||||
return tokenService.createToken(loginUser);
|
||||
}
|
||||
if (hasPromoRole && StringUtils.isNotEmpty(ttUser.getPassword())
|
||||
&& SecurityUtils.matchesPassword(value, ttUser.getPassword())) {
|
||||
return createUserToken(ttUser.getPhoneNumber());
|
||||
}
|
||||
if (promoAdmin != null || hasPromoRole) {
|
||||
throw new IllegalArgumentException("密码错误");
|
||||
}
|
||||
if (ttUser != null) {
|
||||
throw new IllegalArgumentException("该账号无推广后台登录权限");
|
||||
}
|
||||
throw new IllegalArgumentException("验证码错误");
|
||||
}
|
||||
|
||||
// ② 短信验证码
|
||||
if (!isCode) {
|
||||
throw new IllegalArgumentException("验证码错误");
|
||||
}
|
||||
String validateCaptcha = apiSmsService.validateCaptcha(value, "ApiLogin_" + account);
|
||||
if (!"success".equals(validateCaptcha)) {
|
||||
throw new IllegalArgumentException(validateCaptcha);
|
||||
}
|
||||
// 手机号没注册过:带上级邀请码则自动注册为主播(口径与 App 的 verificationCodeLoginOrRegister 一致)
|
||||
autoRegisterAnchorIfNeeded(account, parentInvitationCode);
|
||||
return createUserToken(account);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按手机号或用户名查 tt_user
|
||||
*/
|
||||
private TtUser findUserByAccount(String account) {
|
||||
return new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.and(w -> w.eq(TtUser::getPhoneNumber, account).or().eq(TtUser::getUserName, account))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 推广后台:手机号未注册 + 有效招商邀请码 → 自动注册为主播(01)并挂在招商下
|
||||
*/
|
||||
private void autoRegisterAnchorIfNeeded(String phoneNumber, String parentInvitationCode) {
|
||||
boolean registered = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getPhoneNumber, phoneNumber).count() > 0;
|
||||
if (registered || promoAdminMapper.findByAccount(phoneNumber) != null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isEmpty(parentInvitationCode)) {
|
||||
throw new IllegalArgumentException("该手机号未注册,请填写上级邀请码后再登录");
|
||||
}
|
||||
TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase())
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.eq(TtUser::getStatus, "0").one();
|
||||
if (Objects.isNull(parentUser) || !UserType.BUSINESS_USER.getCode().equals(parentUser.getUserType())) {
|
||||
throw new IllegalArgumentException("上级邀请码填写错误!");
|
||||
}
|
||||
TtUser ttUser = buildPromoUser(phoneNumber, UserType.ANCHOR.getCode(),
|
||||
cn.hutool.core.util.RandomUtil.randomString(10));
|
||||
ttUser.setParentId(parentUser.getUserId());
|
||||
ttUser.setUserId(userIdGenerator.generateUserId());
|
||||
try {
|
||||
ttUserMapper.insert(ttUser);
|
||||
log.info("[推广后台自动注册主播] phone={} parentId={}", phoneNumber, parentUser.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.error("推广后台自动注册主播失败", e);
|
||||
throw new IllegalArgumentException("注册失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createUserToken(String phoneNumber) {
|
||||
sysLoginService.validateBlackIp(phoneNumber);
|
||||
LoginUser loginUser = (LoginUser) apiUserDetailsService.loadUserByUsername("promo_user_" + phoneNumber);
|
||||
// 超级管理员来自 promo_admin 表,其 id 与 tt_user 不同表,不能按 id 回写 tt_user 登录信息
|
||||
if (!UserType.SUPER_ADMIN.getCode().equals(loginUser.getUserData().getUserType())) {
|
||||
recordLoginInfo(loginUser.getUserId());
|
||||
}
|
||||
return tokenService.createToken(loginUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<TtUser> register(RegisterRequest request) {
|
||||
if (StringUtils.isEmpty(request.getUsername())) return R.fail("手机号不能为空");
|
||||
if (!Validator.isMobile(request.getUsername())) return R.fail("手机号格式错误,请检查手机号是否输入正确!");
|
||||
if (StringUtils.isEmpty(request.getPassword())) return R.fail("用户密码不能为空");
|
||||
if (StringUtils.isEmpty(request.getUserType())) return R.fail("注册角色不能为空");
|
||||
if (!NumberUtil.isNumber(request.getCode()) || request.getCode().trim().length() != 4)
|
||||
return R.fail("验证码错误");
|
||||
|
||||
// 推广后台注册始终只允许注册主播(01)账号,任何非主播角色一律拒绝。
|
||||
// 招商(04)由后台「生成招商账号」(TtUserServiceImpl.getAccountList)创建,
|
||||
// 卡商(05)由后台「生成卡商账号」(TtUserServiceImpl.getCardMerchantList)创建,均不经过本接口。
|
||||
String userType = request.getUserType();
|
||||
if (!UserType.ANCHOR.getCode().equals(userType)) {
|
||||
return R.fail("推广后台仅支持注册主播账号");
|
||||
}
|
||||
|
||||
// 主播必须填写上级邀请码
|
||||
if (UserType.ANCHOR.getCode().equals(userType) && StringUtils.isEmpty(request.getParentInvitationCode())) {
|
||||
return R.fail("主播注册必须填写上级邀请码");
|
||||
}
|
||||
|
||||
if (!userService.checkPhoneUnique(TtUser.builder().phoneNumber(request.getUsername()).build()))
|
||||
return R.fail("注册失败," + "手机号'" + request.getUsername() + "'已被注册!");
|
||||
if (!userService.checkUserNameUnique(TtUser.builder().userName(request.getUsername()).build()))
|
||||
return R.fail("注册失败," + "用户名'" + request.getUsername() + "'已存在!");
|
||||
|
||||
String phoneNumber = request.getUsername();
|
||||
TtUser ttUser = buildPromoUser(phoneNumber, userType, request.getPassword());
|
||||
|
||||
// 主播需要验证上级邀请码(必须是招商的邀请码)
|
||||
if (UserType.ANCHOR.getCode().equals(userType)) {
|
||||
var parentInvitationCode = request.getParentInvitationCode();
|
||||
TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase())
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.eq(TtUser::getStatus, "0").one();
|
||||
if (Objects.isNull(parentUser) || !UserType.BUSINESS_USER.getCode().equals(parentUser.getUserType())) {
|
||||
return R.fail("上级邀请码填写错误!");
|
||||
}
|
||||
ttUser.setParentId(parentUser.getUserId());
|
||||
}
|
||||
|
||||
String validateCaptcha = apiSmsService.validateCaptcha(request.getCode().trim(), "ApiRegister_" + phoneNumber);
|
||||
if (!"success".equals(validateCaptcha)) return R.fail(validateCaptcha);
|
||||
|
||||
try {
|
||||
var id = userIdGenerator.generateUserId();
|
||||
ttUser.setUserId(id);
|
||||
ttUserMapper.insert(ttUser);
|
||||
} catch (DuplicateKeyException e) {
|
||||
return R.fail("手机号'" + request.getUsername() + "'已被注册!");
|
||||
} catch (Exception e) {
|
||||
log.error("用户注册失败", e);
|
||||
return R.fail("注册失败,请稍后重试");
|
||||
}
|
||||
ttUser.setPassword(null);
|
||||
return R.ok(ttUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造推广后台账号(注册与自动注册共用,避免两处口径漂移)
|
||||
*/
|
||||
private TtUser buildPromoUser(String phoneNumber, String userType, String rawPassword) {
|
||||
String nickName = RandomUtils.getRandomName(new Random().nextInt(2));
|
||||
TtUser ttUser = TtUser.builder().build();
|
||||
ttUser.setUserName(phoneNumber);
|
||||
ttUser.setNickName(nickName);
|
||||
ttUser.setUserType(userType);
|
||||
ttUser.setPhoneNumber(phoneNumber);
|
||||
List<TtUserAvatar> userAvatarList = new LambdaQueryChainWrapper<>(userAvatarService.getBaseMapper())
|
||||
.eq(TtUserAvatar::getIsDefault, "1").list();
|
||||
if (!userAvatarList.isEmpty()) {
|
||||
ttUser.setAvatar(RandomUtil.choice(userAvatarList).getAvatar());
|
||||
} else {
|
||||
ttUser.setAvatar("");
|
||||
}
|
||||
ttUser.setPassword(SecurityUtils.encryptPassword(rawPassword));
|
||||
ttUser.setInvitationCode(getInvitationCode().toLowerCase());
|
||||
ttUser.setIsRealCheck("0");
|
||||
ttUser.setRemark("明文密码:" + rawPassword);
|
||||
ttUser.setCreateBy("");
|
||||
ttUser.setCreateTime(DateUtils.getNowDate());
|
||||
ttUser.setAccountAmount(BigDecimal.ZERO);
|
||||
ttUser.setAccountCredits(BigDecimal.ZERO);
|
||||
|
||||
// 主播(01)默认返点率:邀请新人充值2、新游戏消费1、经典玩法4
|
||||
if (UserType.ANCHOR.getCode().equals(userType)) {
|
||||
ttUser.setCommissionRate(BigDecimal.valueOf(0));
|
||||
ttUser.setNewGameCommissionRate(BigDecimal.valueOf(1));
|
||||
ttUser.setClassicGameCommissionRate(BigDecimal.valueOf(3.5));
|
||||
}
|
||||
// 招商(04)默认返点率:邀请新人充值0、新游戏消费0.5、经典玩法0.5
|
||||
// 注:注册接口已收紧为仅允许主播(01),此分支为不可达代码,保留仅为减少改动面。
|
||||
else if (UserType.BUSINESS_USER.getCode().equals(userType)) {
|
||||
ttUser.setCommissionRate(BigDecimal.valueOf(0));
|
||||
ttUser.setNewGameCommissionRate(new BigDecimal("0.5"));
|
||||
ttUser.setClassicGameCommissionRate(new BigDecimal("0.5"));
|
||||
}
|
||||
return ttUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<TtUser> updateInvitationCode(UpdateInvitationCodeRequest request) {
|
||||
// 验证参数
|
||||
if (StringUtils.isEmpty(request.getUsername())) return R.fail("用户名不能为空");
|
||||
if (StringUtils.isEmpty(request.getInvitationCode())) return R.fail("邀请码不能为空");
|
||||
if (request.getInvitationCode().length() != 6) return R.fail("邀请码长度必须为 6 位");
|
||||
|
||||
// 从安全上下文中获取当前登录用户 ID,只允许修改自己的邀请码
|
||||
Long currentUserId = SecurityUtils.getUserId();
|
||||
|
||||
// 校验目标用户必须是当前登录用户
|
||||
TtUser user = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getUserId, currentUserId)
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.one();
|
||||
|
||||
if (Objects.isNull(user)) {
|
||||
return R.fail("用户不存在");
|
||||
}
|
||||
|
||||
// 额外校验:如果按 username 传入,确保 username 属于当前用户
|
||||
if (!Objects.equals(user.getUserName(), request.getUsername())) {
|
||||
return R.fail("用户名不匹配,无权修改他人邀请码");
|
||||
}
|
||||
|
||||
// 检查是否已经修改过邀请码(只允许修改一次)
|
||||
String modifiedKey = INVITATION_CODE_MODIFIED_KEY + user.getUserId();
|
||||
if (Boolean.TRUE.equals(redisCache.hasKey(modifiedKey))) {
|
||||
return R.fail("邀请码只能修改一次");
|
||||
}
|
||||
|
||||
// 检查邀请码是否已存在
|
||||
TtUser existingUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getInvitationCode, request.getInvitationCode().toLowerCase())
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.ne(TtUser::getUserId, user.getUserId()) // 排除当前用户
|
||||
.one();
|
||||
|
||||
if (Objects.nonNull(existingUser)) {
|
||||
return R.fail("邀请码已存在,请更换其他邀请码");
|
||||
}
|
||||
|
||||
// 更新邀请码
|
||||
try {
|
||||
boolean updated = new LambdaUpdateChainWrapper<>(ttUserMapper)
|
||||
.eq(TtUser::getUserId, user.getUserId())
|
||||
.set(TtUser::getInvitationCode, request.getInvitationCode().toLowerCase())
|
||||
.update();
|
||||
|
||||
if (!updated) {
|
||||
return R.fail("更新邀请码失败");
|
||||
}
|
||||
|
||||
// 标记该用户已修改过邀请码(永久标记)
|
||||
redisCache.setCacheObject(modifiedKey, "1");
|
||||
|
||||
// 重新获取用户信息
|
||||
user = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
|
||||
.eq(TtUser::getUserId, user.getUserId())
|
||||
.one();
|
||||
user.setPassword(null); // 清除密码
|
||||
return R.ok(user);
|
||||
} catch (Exception e) {
|
||||
log.error("更新邀请码失败", e);
|
||||
return R.fail("更新邀请码失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已修改过邀请码
|
||||
*/
|
||||
public boolean isInvitationCodeModified(Integer userId) {
|
||||
String modifiedKey = INVITATION_CODE_MODIFIED_KEY + userId;
|
||||
return Boolean.TRUE.equals(redisCache.hasKey(modifiedKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public R<GetShareDomainResponse> getShareDomain() {
|
||||
GetShareDomainResponse resp = new GetShareDomainResponse();
|
||||
SysConfig zhubo = sysConfigMapper.checkConfigKeyUnique("app.zhubo.sharelink");
|
||||
SysConfig zhaoshang = sysConfigMapper.checkConfigKeyUnique("app.zhaoshang.sharelink");
|
||||
SysConfig user = sysConfigMapper.checkConfigKeyUnique("app.user.sharelink");
|
||||
if (zhubo != null) {
|
||||
resp.setZhuboShareDomain(zhubo.getConfigValue());
|
||||
}
|
||||
if (zhaoshang != null) {
|
||||
resp.setZhaoshaoShareDomain(zhaoshang.getConfigValue());
|
||||
}
|
||||
if (user != null) {
|
||||
resp.setUserShareDomain(user.getConfigValue());
|
||||
}
|
||||
return R.ok(resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TtRechargeProd> queryList() {
|
||||
LambdaQueryWrapper<TtRechargeProd> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.ge(TtRechargeProd::getPrice, BigDecimal.valueOf(100));
|
||||
return rechargeListMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<String> generateCard(Integer userId, Integer rechargeListId, Integer num) {
|
||||
if (num == null || num <= 0) return new ArrayList<>();
|
||||
|
||||
// 添加上限保护,防止资源耗尽攻击
|
||||
if (num > 100) {
|
||||
log.warn("生成卡片数量超限:userId={}, num={}", userId, num);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
TtRechargeProd recharge = rechargeListMapper.selectById(rechargeListId);
|
||||
if (recharge == null) return new ArrayList<>();
|
||||
|
||||
BigDecimal totalPrice = recharge.getPrice().multiply(BigDecimal.valueOf(num));
|
||||
var r = userService.updateUserCredits(userId, totalPrice.negate(), TtAccountRecordSource.ZHUBO_EXTRACT);
|
||||
if (R.isError(r)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> cardList = new ArrayList<>();
|
||||
List<TtRedPacket> rpList = new ArrayList<>();
|
||||
for (int i = 0; i < num; i++) {
|
||||
TtRedPacket redPacket = new TtRedPacket();
|
||||
redPacket.setNum(1);
|
||||
redPacket.setUserId(userId);
|
||||
redPacket.setTitle("福利");
|
||||
redPacket.setOpeningTime(DateUtils.getNowDate());
|
||||
redPacket.setAmount(String.format("[%s,%s]", recharge.getPrice().toPlainString(), recharge.getPrice().toPlainString()));
|
||||
redPacket.setPassword(IdUtils.fastSimpleUUID().toUpperCase());
|
||||
cardList.add(redPacket.getPassword());
|
||||
redPacket.setCreateBy(SecurityUtils.getUsername());
|
||||
redPacket.setCreateTime(DateUtils.getNowDate());
|
||||
redPacket.setUseStatus(0);
|
||||
rpList.add(redPacket);
|
||||
}
|
||||
|
||||
redPacketMapper.insert(rpList);
|
||||
return cardList;
|
||||
} catch (Exception e) {
|
||||
log.error("生成卡片失败:userId={}, rechargeListId={}, num={}", userId, rechargeListId, num, e);
|
||||
// 扣款与发卡已在同一事务内(方法上的 @Transactional),异常时整体回滚即可,
|
||||
// 不需要再手工补款:手工补款会额外写一条退还流水污染账目,且进程中途异常时补不回来
|
||||
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public String getInvitationCode() {
|
||||
while (true) {
|
||||
try {
|
||||
String randomInvitationCode = UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase();
|
||||
TtUser ttUser = new LambdaQueryChainWrapper<>(ttUserMapper)
|
||||
.eq(TtUser::getInvitationCode, randomInvitationCode)
|
||||
.eq(TtUser::getDelFlag, "0")
|
||||
.one();
|
||||
if (StringUtils.isNull(ttUser)) {
|
||||
return randomInvitationCode;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录登录信息
|
||||
*/
|
||||
public void recordLoginInfo(Long userId) {
|
||||
new LambdaUpdateChainWrapper<>(ttUserMapper).eq(TtUser::getUserId, userId)
|
||||
.set(TtUser::getLoginIp, IpUtils.getIpAddr())
|
||||
.set(TtUser::getLoginDate, DateUtils.getNowDate()).update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.ruoyi.promo.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.promo.domain.entity.PromoMenuConfig;
|
||||
import com.ruoyi.promo.domain.entity.PromoRoleMenu;
|
||||
import com.ruoyi.promo.mapper.PromoMenuConfigMapper;
|
||||
import com.ruoyi.promo.mapper.PromoRoleMenuMapper;
|
||||
import com.ruoyi.promo.service.PromoMenuService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PromoMenuServiceImpl implements PromoMenuService {
|
||||
|
||||
@Autowired
|
||||
private PromoRoleMenuMapper promoRoleMenuMapper;
|
||||
|
||||
@Autowired
|
||||
private PromoMenuConfigMapper promoMenuConfigMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getMenuPathsByUserType(String userType) {
|
||||
// 从数据库获取该角色类型的菜单配置
|
||||
List<String> paths = promoRoleMenuMapper.getMenuPathsByRoleType(userType);
|
||||
if (paths == null || paths.isEmpty()) {
|
||||
// 超级管理员(99)和超管(00)没有配置时,默认返回所有菜单
|
||||
if ("99".equals(userType) || "00".equals(userType)) {
|
||||
return getAllMenuPaths();
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void saveRoleMenus(String roleType, List<String> menuPaths) {
|
||||
// 先删除该角色原有的菜单配置
|
||||
promoRoleMenuMapper.delete(
|
||||
new LambdaQueryWrapper<PromoRoleMenu>().eq(PromoRoleMenu::getRoleType, roleType)
|
||||
);
|
||||
|
||||
// 批量插入新的菜单配置(数组顺序即排序顺序)
|
||||
if (menuPaths != null && !menuPaths.isEmpty()) {
|
||||
for (int i = 0; i < menuPaths.size(); i++) {
|
||||
PromoRoleMenu menu = PromoRoleMenu.builder()
|
||||
.roleType(roleType)
|
||||
.menuPath(menuPaths.get(i))
|
||||
.sortOrder(i)
|
||||
.createTime(DateUtils.getNowDate())
|
||||
.build();
|
||||
promoRoleMenuMapper.insert(menu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRoleMenuPaths(String roleType) {
|
||||
return promoRoleMenuMapper.getMenuPathsByRoleType(roleType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PromoMenuConfig> getAllMenuConfigs() {
|
||||
LambdaQueryWrapper<PromoMenuConfig> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.orderByAsc(PromoMenuConfig::getSortOrder);
|
||||
return promoMenuConfigMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库获取所有菜单路径(带父路径展开)
|
||||
*/
|
||||
private List<String> getAllMenuPaths() {
|
||||
List<PromoMenuConfig> configs = getAllMenuConfigs();
|
||||
List<String> paths = new ArrayList<>();
|
||||
for (PromoMenuConfig config : configs) {
|
||||
String menuPath = config.getMenuPath();
|
||||
// 添加父路径
|
||||
String parentPath = menuPath.substring(0, menuPath.lastIndexOf('/'));
|
||||
if (!parentPath.isEmpty() && !paths.contains(parentPath)) {
|
||||
paths.add(parentPath);
|
||||
}
|
||||
// 添加子路径
|
||||
if (!paths.contains(menuPath)) {
|
||||
paths.add(menuPath);
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
package com.ruoyi.promo.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
|
||||
import com.ruoyi.admin.mapper.TtUserMapper;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.utils.DateTimeUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.bean.BeanUtils;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordType;
|
||||
import com.ruoyi.domain.common.constant.UserType;
|
||||
import com.ruoyi.domain.entity.TtCommissionRecord;
|
||||
import com.ruoyi.domain.entity.TtUserBlendErcash;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.contract.request.GetPurchaseByUserIdRequest;
|
||||
import com.ruoyi.promo.contract.request.GetSubBranchesRequest;
|
||||
import com.ruoyi.promo.contract.request.QueryPromoteWelfareRequest;
|
||||
import com.ruoyi.promo.contract.response.GetSubBranchesResponse;
|
||||
import com.ruoyi.promo.contract.response.QueryPromotionWelfareResponse;
|
||||
import com.ruoyi.promo.contract.vo.DailyUserErcashVO;
|
||||
import com.ruoyi.promo.domain.dto.AnchorDayTurnoverDTO;
|
||||
import com.ruoyi.promo.domain.param.GetPlayersTurnoverParam;
|
||||
import com.ruoyi.promo.domain.vo.AnchorDayTurnoverVO;
|
||||
import com.ruoyi.promo.domain.vo.DayInviteVO;
|
||||
import com.ruoyi.promo.domain.vo.DayTurnoverVO;
|
||||
import com.ruoyi.promo.mapper.PromoTurnoverMapper;
|
||||
import com.ruoyi.promo.service.PromoTurnoverService;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PromoTurnoverServiceImpl implements PromoTurnoverService {
|
||||
|
||||
@Autowired
|
||||
private PromoTurnoverMapper promoTurnoverMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserBlendErcashMapper userBlendErcashMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserMapper userMapper;
|
||||
|
||||
@Override
|
||||
public AjaxResult getRealTimeData(Integer userId) {
|
||||
// 确定当前用户类型,决定直属下级类型
|
||||
TtUser currentUser = userMapper.selectById(userId);
|
||||
String childType = UserType.COMMON_USER.getCode(); // 默认查询普通用户
|
||||
if (currentUser != null) {
|
||||
if (UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) {
|
||||
childType = UserType.ANCHOR.getCode(); // 招商的下级是主播
|
||||
} else if (UserType.ANCHOR.getCode().equals(currentUser.getUserType())) {
|
||||
childType = UserType.COMMON_USER.getCode(); // 主播的下级是普通用户
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有下级用户ID(一次递归,替代原来的 recursionAllPlayer + getAllSubUserIdsRecursive 两次遍历)
|
||||
List<Integer> allSubUserIds = new ArrayList<>();
|
||||
getAllSubUserIdsRecursive(userId, allSubUserIds);
|
||||
|
||||
// 招商用户:游戏消费只算普通用户(不含主播),充值仍算全部下级
|
||||
boolean isBusiness = currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType());
|
||||
List<Integer> gameUserIds = allSubUserIds;
|
||||
if (isBusiness && !allSubUserIds.isEmpty()) {
|
||||
Set<Integer> anchorIds = new LambdaQueryChainWrapper<>(userMapper)
|
||||
.in(TtUser::getUserId, allSubUserIds)
|
||||
.eq(TtUser::getUserType, UserType.ANCHOR.getCode())
|
||||
.list()
|
||||
.stream().map(TtUser::getUserId).collect(Collectors.toSet());
|
||||
gameUserIds = allSubUserIds.stream()
|
||||
.filter(id -> !anchorIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
if (gameUserIds.isEmpty()) {
|
||||
gameUserIds = Collections.singletonList(userId); // 避免空列表导致SQL报错
|
||||
}
|
||||
}
|
||||
|
||||
// 时间范围
|
||||
Date todayStart = getStartOfToday().getTime();
|
||||
Date todayEnd = new Date(todayStart.getTime() + 86400000L);
|
||||
Date yesterdayStart = getStartOfYesterday().getTime();
|
||||
Date yesterdayEnd = new Date(yesterdayStart.getTime() + 86400000L);
|
||||
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
|
||||
// === 新增下级(招商→主播数/天,主播→普通用户数/天)===
|
||||
map.put("todayNewSub", promoTurnoverMapper.countNewSubByDate(userId, childType, todayStart, todayEnd));
|
||||
map.put("yesterdayNewSub", promoTurnoverMapper.countNewSubByDate(userId, childType, yesterdayStart, yesterdayEnd));
|
||||
|
||||
// === 总佣金(新人充值×充值返点比例 + 新游戏消费×新游戏返点 + 经典玩法消费×经典返点)===
|
||||
BigDecimal hundred = BigDecimal.valueOf(100);
|
||||
BigDecimal commissionRate = currentUser != null && currentUser.getCommissionRate() != null
|
||||
? currentUser.getCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
|
||||
BigDecimal newGameRate = currentUser != null && currentUser.getNewGameCommissionRate() != null
|
||||
? currentUser.getNewGameCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
|
||||
BigDecimal classicGameRate = currentUser != null && currentUser.getClassicGameCommissionRate() != null
|
||||
? currentUser.getClassicGameCommissionRate().divide(hundred, 6, BigDecimal.ROUND_HALF_UP) : BigDecimal.ZERO;
|
||||
|
||||
BigDecimal todayCommission = BigDecimal.ZERO;
|
||||
BigDecimal yesterdayCommission = BigDecimal.ZERO;
|
||||
|
||||
if (CollectionUtils.isNotEmpty(allSubUserIds)) {
|
||||
// === 佣金数据:一次查询替代原来6次(充值×2 + 新游戏×2 + 经典×2)===
|
||||
Map<String, Object> commData = promoTurnoverMapper.getBatchedCommissionData(
|
||||
allSubUserIds, gameUserIds, todayStart, todayEnd, yesterdayStart, yesterdayEnd);
|
||||
BigDecimal todaySubRecharge = toBd(commData, "todayRecharge");
|
||||
BigDecimal yesterdaySubRecharge = toBd(commData, "yesterdayRecharge");
|
||||
BigDecimal todayNewGameConsume = toBd(commData, "todayNewGame");
|
||||
BigDecimal yesterdayNewGameConsume = toBd(commData, "yesterdayNewGame");
|
||||
BigDecimal todayClassicConsume = toBd(commData, "todayClassic");
|
||||
BigDecimal yesterdayClassicConsume = toBd(commData, "yesterdayClassic");
|
||||
|
||||
todayCommission = commissionRate.multiply(todaySubRecharge)
|
||||
.add(newGameRate.multiply(todayNewGameConsume))
|
||||
.add(classicGameRate.multiply(todayClassicConsume));
|
||||
yesterdayCommission = commissionRate.multiply(yesterdaySubRecharge)
|
||||
.add(newGameRate.multiply(yesterdayNewGameConsume))
|
||||
.add(classicGameRate.multiply(yesterdayClassicConsume));
|
||||
|
||||
// === 下级充值(复用上面的批量结果)===
|
||||
map.put("todaySubRecharge", todaySubRecharge);
|
||||
map.put("yesterdaySubRecharge", yesterdaySubRecharge);
|
||||
|
||||
// === 所有下级总充值(终身累计,时间边界传 null)===
|
||||
BigDecimal totalSubRecharge = promoTurnoverMapper.getSubTotalRecharge(allSubUserIds, null, null);
|
||||
map.put("totalSubRecharge", totalSubRecharge != null ? totalSubRecharge : BigDecimal.ZERO);
|
||||
|
||||
// === 下级收益(所有下级用户的提货总价值)===
|
||||
BigDecimal todaySubIncome = promoTurnoverMapper.getSubDeliveryValueByDate(allSubUserIds, todayStart, todayEnd);
|
||||
BigDecimal yesterdaySubIncome = promoTurnoverMapper.getSubDeliveryValueByDate(allSubUserIds, yesterdayStart, yesterdayEnd);
|
||||
map.put("todaySubIncome", todaySubIncome != null ? todaySubIncome : BigDecimal.ZERO);
|
||||
map.put("yesterdaySubIncome", yesterdaySubIncome != null ? yesterdaySubIncome : BigDecimal.ZERO);
|
||||
|
||||
// 查询名下主播数
|
||||
map.put("anchorCount", promoTurnoverMapper.getAnchorCount(userId));
|
||||
|
||||
// === 消费流水:招商=下级主播+主播下级(普通用户);主播=下级普通用户,均取 allSubUserIds ===
|
||||
Map<String, Object> batched = promoTurnoverMapper.getBatchedTurnover(
|
||||
allSubUserIds, new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes()));
|
||||
if (batched != null) {
|
||||
map.put("totalTurnover", batched.get("totalTurnover"));
|
||||
map.put("lastMonthTurnover", batched.get("lastMonth"));
|
||||
map.put("currentMonthTurnover", batched.get("currentMonth"));
|
||||
map.put("lastWeekTurnover", batched.get("lastWeek"));
|
||||
map.put("currentWeekTurnover", batched.get("currentWeek"));
|
||||
map.put("lastDayTurnover", batched.get("lastDay"));
|
||||
map.put("currentDayTurnover", batched.get("currentDay"));
|
||||
} else {
|
||||
map.put("totalTurnover", BigDecimal.ZERO);
|
||||
map.put("lastMonthTurnover", BigDecimal.ZERO);
|
||||
map.put("currentMonthTurnover", BigDecimal.ZERO);
|
||||
map.put("lastWeekTurnover", BigDecimal.ZERO);
|
||||
map.put("currentWeekTurnover", BigDecimal.ZERO);
|
||||
map.put("lastDayTurnover", BigDecimal.ZERO);
|
||||
map.put("currentDayTurnover", BigDecimal.ZERO);
|
||||
}
|
||||
} else {
|
||||
map.put("anchorCount", 0);
|
||||
map.put("totalTurnover", BigDecimal.ZERO);
|
||||
map.put("lastMonthTurnover", BigDecimal.ZERO);
|
||||
map.put("currentMonthTurnover", BigDecimal.ZERO);
|
||||
map.put("lastWeekTurnover", BigDecimal.ZERO);
|
||||
map.put("currentWeekTurnover", BigDecimal.ZERO);
|
||||
map.put("lastDayTurnover", BigDecimal.ZERO);
|
||||
map.put("currentDayTurnover", BigDecimal.ZERO);
|
||||
map.put("todaySubRecharge", BigDecimal.ZERO);
|
||||
map.put("yesterdaySubRecharge", BigDecimal.ZERO);
|
||||
map.put("totalSubRecharge", BigDecimal.ZERO);
|
||||
map.put("todaySubIncome", BigDecimal.ZERO);
|
||||
map.put("yesterdaySubIncome", BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
map.put("todayCommission", todayCommission);
|
||||
map.put("yesterdayCommission", yesterdayCommission);
|
||||
|
||||
// === 个人消费福利:一次查询替代原来2次(today + yesterday)===
|
||||
Map<String, Object> welfare = promoTurnoverMapper.getBatchedWelfare(
|
||||
userId, todayStart, todayEnd, yesterdayStart, yesterdayEnd);
|
||||
map.put("todayPersonalWelfare", toBd(welfare, "todayWelfare"));
|
||||
map.put("yesterdayPersonalWelfare", toBd(welfare, "yesterdayWelfare"));
|
||||
|
||||
return AjaxResult.success(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult getGameCommissionData(Integer userId) {
|
||||
TtUser currentUser = userMapper.selectById(userId);
|
||||
String childType = UserType.COMMON_USER.getCode();
|
||||
boolean isBusiness = false;
|
||||
if (currentUser != null) {
|
||||
if (UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType())) {
|
||||
childType = UserType.ANCHOR.getCode();
|
||||
isBusiness = true;
|
||||
} else if (UserType.ANCHOR.getCode().equals(currentUser.getUserType())) {
|
||||
childType = UserType.COMMON_USER.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
List<Integer> allSubUserIds = new ArrayList<>();
|
||||
getAllSubUserIdsRecursive(userId, allSubUserIds);
|
||||
|
||||
List<Integer> gameUserIds = allSubUserIds;
|
||||
if (isBusiness && !allSubUserIds.isEmpty()) {
|
||||
Set<Integer> anchorIds = new LambdaQueryChainWrapper<>(userMapper)
|
||||
.in(TtUser::getUserId, allSubUserIds)
|
||||
.eq(TtUser::getUserType, UserType.ANCHOR.getCode())
|
||||
.list()
|
||||
.stream().map(TtUser::getUserId).collect(Collectors.toSet());
|
||||
gameUserIds = allSubUserIds.stream()
|
||||
.filter(id -> !anchorIds.contains(id))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
Date todayStart = getStartOfToday().getTime();
|
||||
Date todayEnd = new Date(todayStart.getTime() + 86400000L);
|
||||
Date yesterdayStart = getStartOfYesterday().getTime();
|
||||
Date yesterdayEnd = new Date(yesterdayStart.getTime() + 86400000L);
|
||||
Date[] weekRange = getCurrentWeekRange();
|
||||
Date weekStart = weekRange[0];
|
||||
Date weekEnd = weekRange[1];
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
|
||||
// 当前登录用户的后台返佣比例
|
||||
Map<String, Object> rates = new LinkedHashMap<>();
|
||||
rates.put("rechargeRate", currentUser != null && currentUser.getCommissionRate() != null
|
||||
? currentUser.getCommissionRate() : BigDecimal.ZERO);
|
||||
rates.put("newGameRate", currentUser != null && currentUser.getNewGameCommissionRate() != null
|
||||
? currentUser.getNewGameCommissionRate() : BigDecimal.ZERO);
|
||||
rates.put("classicGameRate", currentUser != null && currentUser.getClassicGameCommissionRate() != null
|
||||
? currentUser.getClassicGameCommissionRate() : BigDecimal.ZERO);
|
||||
result.put("rates", rates);
|
||||
|
||||
// 各游戏消费及充值数据
|
||||
Map<String, Object> data;
|
||||
if (CollectionUtils.isEmpty(allSubUserIds)) {
|
||||
data = createZeroGameCommissionData();
|
||||
} else {
|
||||
List<Integer> effectiveGameUserIds = gameUserIds.isEmpty()
|
||||
? Collections.singletonList(-1) : gameUserIds;
|
||||
data = promoTurnoverMapper.getGameCommissionData(
|
||||
effectiveGameUserIds,
|
||||
allSubUserIds,
|
||||
todayStart, todayEnd,
|
||||
yesterdayStart, yesterdayEnd,
|
||||
weekStart, weekEnd);
|
||||
}
|
||||
|
||||
// 新增下级
|
||||
int todayNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, todayStart, todayEnd);
|
||||
int yesterdayNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, yesterdayStart, yesterdayEnd);
|
||||
int weekNewSub = promoTurnoverMapper.countNewSubByDate(userId, childType, weekStart, weekEnd);
|
||||
int totalNewSub = CollectionUtils.isEmpty(allSubUserIds) ? 0 : allSubUserIds.size();
|
||||
|
||||
BigDecimal rechargeRate = toBd(rates, "rechargeRate");
|
||||
BigDecimal newGameRate = toBd(rates, "newGameRate");
|
||||
BigDecimal classicGameRate = toBd(rates, "classicGameRate");
|
||||
|
||||
// 按周期组装结果:今日/昨日/本周/总
|
||||
List<Map<String, Object>> periods = new ArrayList<>();
|
||||
periods.add(buildPeriod("今日", data,
|
||||
rechargeRate, newGameRate, classicGameRate,
|
||||
"todayClassicBox", "todayLuckyUpgrade", "todayBattle", "todayCrazyWheel", "todayOlympus",
|
||||
"todayRecharge", todayNewSub));
|
||||
periods.add(buildPeriod("昨日", data,
|
||||
rechargeRate, newGameRate, classicGameRate,
|
||||
"yesterdayClassicBox", "yesterdayLuckyUpgrade", "yesterdayBattle", "yesterdayCrazyWheel", "yesterdayOlympus",
|
||||
"yesterdayRecharge", yesterdayNewSub));
|
||||
periods.add(buildPeriod("本周", data,
|
||||
rechargeRate, newGameRate, classicGameRate,
|
||||
"weekClassicBox", "weekLuckyUpgrade", "weekBattle", "weekCrazyWheel", "weekOlympus",
|
||||
"weekRecharge", weekNewSub));
|
||||
periods.add(buildPeriod("总", data,
|
||||
rechargeRate, newGameRate, classicGameRate,
|
||||
"totalClassicBox", "totalLuckyUpgrade", "totalBattle", "totalCrazyWheel", "totalOlympus",
|
||||
"totalRecharge", totalNewSub));
|
||||
|
||||
result.put("periods", periods);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AjaxResult getLast10DaysPromotionData(Integer userId) {
|
||||
// 分离收集下级:主播(01) 用于"主播日流水/主播日邀请",普通用户(02) 用于"主播下级日流水/主播下级日邀请"
|
||||
List<Integer> anchorIds = new ArrayList<>();
|
||||
List<Integer> subUserIds = new ArrayList<>();
|
||||
collectAnchorAndSubIds(userId, anchorIds, subUserIds);
|
||||
if (CollectionUtils.isEmpty(anchorIds) && CollectionUtils.isEmpty(subUserIds)) {
|
||||
return AjaxResult.success(new ArrayList<>());
|
||||
}
|
||||
|
||||
List<Integer> sourceCodes = new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes());
|
||||
|
||||
Map<String, BigDecimal> anchorTurnoverMap = CollectionUtils.isEmpty(anchorIds)
|
||||
? Collections.emptyMap()
|
||||
: toTurnoverDayMap(promoTurnoverMapper.getLast10DaysTurnover(anchorIds, sourceCodes));
|
||||
Map<String, BigDecimal> subTurnoverMap = CollectionUtils.isEmpty(subUserIds)
|
||||
? Collections.emptyMap()
|
||||
: toTurnoverDayMap(promoTurnoverMapper.getLast10DaysTurnover(subUserIds, sourceCodes));
|
||||
Map<String, Integer> anchorInviteMap = CollectionUtils.isEmpty(anchorIds)
|
||||
? Collections.emptyMap()
|
||||
: toInviteDayMap(promoTurnoverMapper.getLast10DaysInviteByUserIds(anchorIds));
|
||||
Map<String, Integer> subInviteMap = CollectionUtils.isEmpty(subUserIds)
|
||||
? Collections.emptyMap()
|
||||
: toInviteDayMap(promoTurnoverMapper.getLast10DaysInviteByUserIds(subUserIds));
|
||||
|
||||
// 各查询均为近10天 date_range 左连接,返回同一组日期;合并后升序作为横轴
|
||||
Set<String> dates = new TreeSet<>();
|
||||
dates.addAll(anchorTurnoverMap.keySet());
|
||||
dates.addAll(subTurnoverMap.keySet());
|
||||
dates.addAll(anchorInviteMap.keySet());
|
||||
dates.addAll(subInviteMap.keySet());
|
||||
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (String date : dates) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("date", date);
|
||||
map.put("anchorDayTurnover", anchorTurnoverMap.getOrDefault(date, BigDecimal.ZERO));
|
||||
map.put("subDayTurnover", subTurnoverMap.getOrDefault(date, BigDecimal.ZERO));
|
||||
map.put("anchorDayInvite", anchorInviteMap.getOrDefault(date, 0));
|
||||
map.put("subDayInvite", subInviteMap.getOrDefault(date, 0));
|
||||
list.add(map);
|
||||
}
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归分离收集下级用户ID:
|
||||
* 主播(01) 归入 anchorIds 并继续下钻,普通用户(02) 归入 subUserIds。
|
||||
*/
|
||||
private void collectAnchorAndSubIds(int parentId, List<Integer> anchorIds, List<Integer> subUserIds) {
|
||||
List<TtUser> users = promoTurnoverMapper.findByParentId(parentId);
|
||||
for (TtUser user : users) {
|
||||
if (UserType.ANCHOR.getCode().equals(user.getUserType())) {
|
||||
anchorIds.add(user.getUserId());
|
||||
collectAnchorAndSubIds(user.getUserId(), anchorIds, subUserIds);
|
||||
} else if (UserType.COMMON_USER.getCode().equals(user.getUserType())) {
|
||||
subUserIds.add(user.getUserId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, BigDecimal> toTurnoverDayMap(List<DayTurnoverVO> list) {
|
||||
Map<String, BigDecimal> map = new HashMap<>();
|
||||
if (list == null) return map;
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (DayTurnoverVO vo : list) {
|
||||
map.put(sdf.format(vo.getDate()), vo.getAmount());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private Map<String, Integer> toInviteDayMap(List<DayInviteVO> list) {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
if (list == null) return map;
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
for (DayInviteVO vo : list) {
|
||||
map.put(sdf.format(vo.getDate()), vo.getAnchorCount());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AnchorDayTurnoverVO> getAnchorDayTurnover(AnchorDayTurnoverDTO anchorDayTurnoverDTO) {
|
||||
// 递归名下所有玩家
|
||||
Integer userId = anchorDayTurnoverDTO.getUserId();
|
||||
List<TtUser> allPlayer = recursionAllPlayer(userId);
|
||||
List<Integer> userIds = new ArrayList<>();
|
||||
for (TtUser ttUser : allPlayer) {
|
||||
userIds.add(ttUser.getUserId());
|
||||
}
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
anchorDayTurnoverDTO.setUserIds(userIds);
|
||||
anchorDayTurnoverDTO.setSourceCodes(new ArrayList<>(TtAccountRecordSource.getWelfarePrizeCodes()));
|
||||
|
||||
List<AnchorDayTurnoverVO> result = promoTurnoverMapper.getAnchorDayTurnover(anchorDayTurnoverDTO);
|
||||
for (AnchorDayTurnoverVO anchorDayTurnoverVO : result) {
|
||||
Date startDate = anchorDayTurnoverVO.getDate();
|
||||
Date endDate = DateTimeUtils.addDays(startDate, 1);
|
||||
BigDecimal dailyTotalRecharge = promoTurnoverMapper.getDailyTotalRecharge(anchorDayTurnoverVO.getUserId(), startDate, endDate);
|
||||
anchorDayTurnoverVO.setDailyRechargeTotal(dailyTotalRecharge == null ? BigDecimal.ZERO : dailyTotalRecharge);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetSubBranchesResponse getSubBranches(GetSubBranchesRequest request) {
|
||||
if (request.getPageNum() == null || request.getPageNum() <= 0) {
|
||||
request.setPageNum(1);
|
||||
}
|
||||
if (request.getPageSize() == null || request.getPageSize() <= 0) {
|
||||
request.setPageSize(10);
|
||||
}
|
||||
Long userId = Long.valueOf(request.getUserId());
|
||||
var query = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getParentId, userId);
|
||||
|
||||
if (request.getPlayerId() != null) {
|
||||
query.eq(TtUser::getUserId, request.getPlayerId());
|
||||
}
|
||||
|
||||
Page<TtUser> page = new Page<>(request.getPageNum(), request.getPageSize());
|
||||
page.setOptimizeCountSql(false);
|
||||
|
||||
var pageData = query.page(page);
|
||||
|
||||
var childrenList = pageData.getRecords();
|
||||
|
||||
if (CollectionUtils.isEmpty(childrenList)) {
|
||||
GetSubBranchesResponse response = new GetSubBranchesResponse();
|
||||
response.setTotal((int) page.getTotal());
|
||||
response.setRows(new ArrayList<>());
|
||||
return response;
|
||||
}
|
||||
|
||||
List<Integer> userids = childrenList.stream().map(TtUser::getUserId).collect(Collectors.toList());
|
||||
List<TtUserBlendErcash> ercashList = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
|
||||
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes())
|
||||
.in(TtUserBlendErcash::getUserId, userids).list();
|
||||
Map<Integer, List<TtUserBlendErcash>> ercashMap = ercashList.stream()
|
||||
.collect(Collectors.groupingBy(TtUserBlendErcash::getUserId));
|
||||
|
||||
List<GetSubBranchesResponse.UserInfo> resultList = childrenList.stream().map(user -> {
|
||||
GetSubBranchesResponse.UserInfo userInfo = new GetSubBranchesResponse.UserInfo();
|
||||
BeanUtils.copyProperties(user, userInfo);
|
||||
|
||||
userInfo.setTotalCost(BigDecimal.ZERO);
|
||||
userInfo.setTotalRecharge(BigDecimal.ZERO);
|
||||
|
||||
if (ercashMap.containsKey(user.getUserId())) {
|
||||
var records = ercashMap.get(user.getUserId());
|
||||
|
||||
var totalRecharge = records.stream().filter(v -> {
|
||||
return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
var allConsumeList = records.stream().filter(v -> {
|
||||
return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode());
|
||||
}).toList();
|
||||
|
||||
var totalNormalGameConsume = allConsumeList.stream().filter(v -> {
|
||||
return TtAccountRecordSource.isNormalGameConsume(v.getSource());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
var totalSpecialGameConsume = allConsumeList.stream().filter(v -> {
|
||||
return TtAccountRecordSource.isSpecialGameConsume(v.getSource());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
|
||||
userInfo.setTotalNormGameCost(totalNormalGameConsume);
|
||||
userInfo.setTotalSpecialGameCost(totalSpecialGameConsume);
|
||||
userInfo.setTotalRecharge(totalRecharge);
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
GetSubBranchesResponse response = new GetSubBranchesResponse();
|
||||
response.setTotal((int) page.getTotal());
|
||||
response.setRows(resultList);
|
||||
return response;
|
||||
}
|
||||
|
||||
private Set<TtUser> findAllChild(Integer parentId, Set<TtUser> result) {
|
||||
|
||||
List<TtUser> list = promoTurnoverMapper.findByParentId(parentId);
|
||||
HashSet<TtUser> childrens = new HashSet<>(list);
|
||||
|
||||
if (childrens.isEmpty()) return result;
|
||||
|
||||
result.addAll(childrens);
|
||||
for (TtUser child : childrens) {
|
||||
Set<TtUser> allChild = findAllChild(child.getUserId(), result);
|
||||
result.addAll(allChild);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo getPurchaseByUserId(GetPurchaseByUserIdRequest request) {
|
||||
if (request.getPageNum() == null || request.getPageNum() <= 0) {
|
||||
request.setPageNum(1);
|
||||
}
|
||||
if (request.getPageSize() == null || request.getPageSize() <= 0) {
|
||||
request.setPageSize(10);
|
||||
}
|
||||
if (request.getBeginTime() == null) {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
LocalDateTime startTime = endTime.minusDays(30).with(LocalTime.MIN);
|
||||
request.setBeginTime(DateTimeUtils.date(startTime));
|
||||
}
|
||||
if (request.getEndTime() == null) {
|
||||
LocalDateTime endTime = LocalDateTime.now();
|
||||
request.setEndTime(DateTimeUtils.date(endTime));
|
||||
}
|
||||
// Limit the time range to maximum 30 days
|
||||
if (request.getBeginTime() != null && request.getEndTime() != null) {
|
||||
LocalDateTime begin = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, request.getBeginTime()));
|
||||
LocalDateTime end = DateTimeUtils.toLocalDateTime(DateTimeUtils.dateTime(DateTimeUtils.YYYY_MM_DD, request.getEndTime()));
|
||||
if (ChronoUnit.DAYS.between(begin, end) > 30) {
|
||||
LocalDateTime adjustedEnd = begin.plusDays(30);
|
||||
request.setEndTime(DateTimeUtils.date(adjustedEnd));
|
||||
}
|
||||
}
|
||||
|
||||
// 权限校验:只能查询自己下级的流水
|
||||
Long currentUserId = SecurityUtils.getUserId();
|
||||
Integer queryUserId = request.getUserId();
|
||||
|
||||
// 招商账号:可以查所有下级(递归)
|
||||
// 主播账号:只能查自己或自己的直属下级
|
||||
TtUser currentUser = userMapper.selectById(currentUserId.intValue());
|
||||
boolean isBusiness = currentUser != null && UserType.BUSINESS_USER.getCode().equals(currentUser.getUserType());
|
||||
|
||||
if (!isBusiness) {
|
||||
// 主播账号:只能查自己
|
||||
if (!queryUserId.equals(currentUserId.intValue())) {
|
||||
var result = new TableDataInfo();
|
||||
result.setCode(HttpStatus.ERROR);
|
||||
result.setMsg("无权查看他人数据");
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
// 招商账号:需要校验 queryUserId 是否属于当前招商的下级树
|
||||
List<Integer> allSubUserIds = new ArrayList<>();
|
||||
getAllSubUserIdsRecursive(currentUserId.intValue(), allSubUserIds);
|
||||
if (!allSubUserIds.contains(queryUserId)) {
|
||||
var result = new TableDataInfo();
|
||||
result.setCode(HttpStatus.ERROR);
|
||||
result.setMsg("无权查看该用户数据");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> days = DateTimeUtils.generateDailyDates(request.getBeginTime(), request.getEndTime());
|
||||
days.sort(Comparator.reverseOrder());
|
||||
|
||||
int total = days.size();
|
||||
int start = Math.min((request.getPageNum() - 1) * request.getPageSize(), total - 1);
|
||||
int end = Math.min(start + request.getPageSize(), total);
|
||||
start = Math.max(start, 0);
|
||||
end = Math.max(end, 0);
|
||||
days = days.subList(start, end);
|
||||
|
||||
|
||||
|
||||
List<DailyUserErcashVO> resultList = days.stream().map(d -> {
|
||||
var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
|
||||
.eq(TtUserBlendErcash::getUserId, request.getUserId())
|
||||
.apply("DATE(create_time) = {0}", d)
|
||||
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getWelfarePrizeCodes())
|
||||
.list();
|
||||
var totalRecharge = records.stream().filter(v -> {
|
||||
return Objects.equals(v.getSource(), TtAccountRecordSource.RECHARGE.getCode());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
var allConsumeList = records.stream().filter(v -> {
|
||||
return Objects.equals(v.getType(), TtAccountRecordType.OUTPUT.getCode());
|
||||
}).toList();
|
||||
|
||||
var totalNormalGameConsume = allConsumeList.stream().filter(v -> {
|
||||
return TtAccountRecordSource.isNormalGameConsume(v.getSource());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
var totalSpecialGameConsume = allConsumeList.stream().filter(v -> {
|
||||
return TtAccountRecordSource.isSpecialGameConsume(v.getSource());
|
||||
}).map(v -> v.getAmount().abs()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
DailyUserErcashVO r = new DailyUserErcashVO();
|
||||
r.setDate(d);
|
||||
r.setDailyNormGameCost(totalNormalGameConsume);
|
||||
r.setDailySpecialGameCost(totalSpecialGameConsume);
|
||||
r.setDailyRecharge(totalRecharge);
|
||||
return r;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
TableDataInfo result = new TableDataInfo();
|
||||
result.setCode(200);
|
||||
result.setRows(resultList);
|
||||
result.setTotal(total);
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal getCommissionRateByUserId(Integer userId) {
|
||||
return promoTurnoverMapper.getCommissionRateByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int updateCommissionRate(Integer userId, BigDecimal commissionRate) {
|
||||
return promoTurnoverMapper.updateCommissionRate(userId, commissionRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TtCommissionRecord> getCommissionList(Integer userId) {
|
||||
return promoTurnoverMapper.getCommissionList(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryPromotionWelfareResponse queryPromoteWelfare(Integer userId, QueryPromoteWelfareRequest request) {
|
||||
Page<TtUserBlendErcash> page = new Page<>(request.getPageNum(), request.getPageSize());
|
||||
page.setOptimizeCountSql(false);
|
||||
|
||||
LambdaQueryChainWrapper<TtUserBlendErcash> wrapper = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
|
||||
.eq(TtUserBlendErcash::getUserId, userId)
|
||||
.eq(TtUserBlendErcash::getSource, TtAccountRecordSource.P_WELFARE.getCode());
|
||||
|
||||
if (request.getBeginTime() != null) {
|
||||
wrapper.ge(TtUserBlendErcash::getCreateTime, request.getBeginTime());
|
||||
}
|
||||
if (request.getEndTime() != null) {
|
||||
wrapper.le(TtUserBlendErcash::getCreateTime, request.getEndTime());
|
||||
}
|
||||
wrapper.orderByDesc(TtUserBlendErcash::getCreateTime);
|
||||
Page<TtUserBlendErcash> pageResult = wrapper.page(page);
|
||||
|
||||
BigDecimal totalReward = promoTurnoverMapper.queryTotalRewardBySource(userId, TtAccountRecordSource.P_WELFARE.getCode());
|
||||
if (totalReward == null) {
|
||||
totalReward = BigDecimal.ZERO;
|
||||
}
|
||||
if (CollectionUtils.isEmpty(pageResult.getRecords())) {
|
||||
QueryPromotionWelfareResponse result = new QueryPromotionWelfareResponse();
|
||||
result.setRows(new ArrayList<>());
|
||||
result.setTotal(0);
|
||||
result.setTotalReward(totalReward);
|
||||
return result;
|
||||
}
|
||||
|
||||
QueryPromotionWelfareResponse result = new QueryPromotionWelfareResponse();
|
||||
result.setRows(pageResult.getRecords().stream().map(v -> {
|
||||
QueryPromotionWelfareResponse.Record r = new QueryPromotionWelfareResponse.Record();
|
||||
r.setReward(v.getAmount());
|
||||
r.setDate(DateTimeUtils.dateTime(v.getCreateTime().toLocalDateTime()));
|
||||
return r;
|
||||
}).collect(Collectors.toList()));
|
||||
result.setTotalReward(totalReward);
|
||||
result.setTotal((int) pageResult.getTotal());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归下级分支
|
||||
*/
|
||||
private void buildUserTree(TtUser ttUser) {
|
||||
// 获取当前用户的子节点
|
||||
List<TtUser> children = promoTurnoverMapper.findByParentId(ttUser.getUserId());
|
||||
// 设置当前用户的子节点
|
||||
ttUser.setChildren(children);
|
||||
for (TtUser child : children) {
|
||||
// 如果子节点是主播,继续递归
|
||||
if ("01".equals(child.getUserType())) {
|
||||
buildUserTree(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TtUser> recursionAllPlayer(Integer userId) {
|
||||
List<TtUser> players = new ArrayList<>();
|
||||
getAllPlayersByAnchorIdRecursive(userId, players);
|
||||
return players;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归名下所有玩家(仅普通用户type=02)
|
||||
*/
|
||||
private void getAllPlayersByAnchorIdRecursive(int parentId, List<TtUser> players) {
|
||||
List<TtUser> users = promoTurnoverMapper.findByParentId(parentId);
|
||||
for (TtUser user : users) {
|
||||
if ("02".equals(user.getUserType())) { // 如果是玩家
|
||||
players.add(user);
|
||||
} else if ("01".equals(user.getUserType())) { // 如果是主播,继续递归
|
||||
getAllPlayersByAnchorIdRecursive(user.getUserId(), players);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取所有下级用户ID(包含主播和普通用户)
|
||||
* 招商→主播+普通用户,主播→普通用户
|
||||
*/
|
||||
private void getAllSubUserIdsRecursive(int parentId, List<Integer> subUserIds) {
|
||||
List<TtUser> users = promoTurnoverMapper.findByParentId(parentId);
|
||||
for (TtUser user : users) {
|
||||
subUserIds.add(user.getUserId());
|
||||
if ("01".equals(user.getUserType())) { // 如果是主播,继续递归其下级
|
||||
getAllSubUserIdsRecursive(user.getUserId(), subUserIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getAllSubIdsRecursive(Integer parentId, List<Integer> subUserIds) {
|
||||
getAllSubUserIdsRecursive(parentId, subUserIds);
|
||||
}
|
||||
|
||||
private Calendar getStartOfLastMonth() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MONTH, -1);
|
||||
calendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getEndOfLastMonth() {
|
||||
Calendar calendar = getStartOfLastMonth();
|
||||
calendar.add(Calendar.MONTH, 1);
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -1);
|
||||
setTimeToEndOfDay(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getStartOfThisMonth() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getStartOfLastWeek() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.WEEK_OF_YEAR, -1);
|
||||
calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek());
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getEndOfLastWeek() {
|
||||
Calendar calendar = getStartOfLastWeek();
|
||||
calendar.add(Calendar.DAY_OF_WEEK, 6);
|
||||
setTimeToEndOfDay(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getStartOfThisWeek() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek());
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getStartOfYesterday() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.DAY_OF_MONTH, -1);
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getEndOfYesterday() {
|
||||
Calendar calendar = getStartOfYesterday();
|
||||
setTimeToEndOfDay(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private Calendar getStartOfToday() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
resetTime(calendar);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
private void resetTime(Calendar calendar) {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
}
|
||||
|
||||
private void setTimeToEndOfDay(Calendar calendar) {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 23);
|
||||
calendar.set(Calendar.MINUTE, 59);
|
||||
calendar.set(Calendar.SECOND, 59);
|
||||
calendar.set(Calendar.MILLISECOND, 999);
|
||||
}
|
||||
|
||||
/** 从 Map 中安全取 BigDecimal,null 返回 ZERO */
|
||||
private BigDecimal toBd(Map<String, Object> map, String key) {
|
||||
if (map == null) return BigDecimal.ZERO;
|
||||
Object v = map.get(key);
|
||||
return v instanceof BigDecimal ? (BigDecimal) v : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本周一 00:00:00 到下周周一 00:00:00(左闭右开),与 weeklyDailyCommission 口径一致。
|
||||
*/
|
||||
private Date[] getCurrentWeekRange() {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||
cal.set(Calendar.MINUTE, 0);
|
||||
cal.set(Calendar.SECOND, 0);
|
||||
cal.set(Calendar.MILLISECOND, 0);
|
||||
int daysSinceMonday = (cal.get(Calendar.DAY_OF_WEEK) + 5) % 7;
|
||||
cal.add(Calendar.DAY_OF_MONTH, -daysSinceMonday);
|
||||
Date start = cal.getTime();
|
||||
cal.add(Calendar.DAY_OF_MONTH, 7);
|
||||
Date end = cal.getTime();
|
||||
return new Date[]{start, end};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造无下级时的全零看板数据。
|
||||
*/
|
||||
private Map<String, Object> createZeroGameCommissionData() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] keys = {
|
||||
"todayClassicBox", "todayLuckyUpgrade", "todayBattle", "todayCrazyWheel", "todayOlympus",
|
||||
"yesterdayClassicBox", "yesterdayLuckyUpgrade", "yesterdayBattle", "yesterdayCrazyWheel", "yesterdayOlympus",
|
||||
"weekClassicBox", "weekLuckyUpgrade", "weekBattle", "weekCrazyWheel", "weekOlympus",
|
||||
"totalClassicBox", "totalLuckyUpgrade", "totalBattle", "totalCrazyWheel", "totalOlympus",
|
||||
"todayRecharge", "yesterdayRecharge", "weekRecharge", "totalRecharge"
|
||||
};
|
||||
for (String key : keys) {
|
||||
map.put(key, BigDecimal.ZERO);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装单个周期的看板数据行。
|
||||
*/
|
||||
private Map<String, Object> buildPeriod(String periodName, Map<String, Object> data,
|
||||
BigDecimal rechargeRate, BigDecimal newGameRate, BigDecimal classicGameRate,
|
||||
String classicBoxKey, String luckyUpgradeKey, String battleKey,
|
||||
String crazyWheelKey, String olympusKey, String rechargeKey, int newSub) {
|
||||
BigDecimal classicBox = toBd(data, classicBoxKey);
|
||||
BigDecimal luckyUpgrade = toBd(data, luckyUpgradeKey);
|
||||
BigDecimal battle = toBd(data, battleKey);
|
||||
BigDecimal crazyWheel = toBd(data, crazyWheelKey);
|
||||
BigDecimal olympus = toBd(data, olympusKey);
|
||||
BigDecimal recharge = toBd(data, rechargeKey);
|
||||
BigDecimal totalConsume = classicBox.add(luckyUpgrade).add(battle).add(crazyWheel).add(olympus);
|
||||
BigDecimal classicConsume = classicBox.add(luckyUpgrade).add(battle).add(crazyWheel);
|
||||
BigDecimal totalRebate = calcCommission(recharge, rechargeRate)
|
||||
.add(calcCommission(olympus, newGameRate))
|
||||
.add(calcCommission(classicConsume, classicGameRate));
|
||||
|
||||
Map<String, Object> period = new LinkedHashMap<>();
|
||||
period.put("period", periodName);
|
||||
period.put("classicBox", classicBox);
|
||||
period.put("luckyUpgrade", luckyUpgrade);
|
||||
period.put("battle", battle);
|
||||
period.put("crazyWheel", crazyWheel);
|
||||
period.put("olympus", olympus);
|
||||
period.put("totalConsume", totalConsume);
|
||||
period.put("recharge", recharge);
|
||||
period.put("newSub", newSub);
|
||||
period.put("totalRebate", totalRebate);
|
||||
return period;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按百分比费率计算佣金:amount × rate / 100,保留两位小数。
|
||||
*/
|
||||
private BigDecimal calcCommission(BigDecimal amount, BigDecimal rate) {
|
||||
if (amount == null || rate == null
|
||||
|| amount.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| rate.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return amount.multiply(rate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.ruoyi.promo.task;
|
||||
|
||||
import com.ruoyi.admin.service.TtUserService;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
|
||||
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
|
||||
import com.ruoyi.domain.entity.TtCommissionRecord;
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.mapper.PromoTurnoverMapper;
|
||||
import com.ruoyi.message.service.MessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 招商佣金周结定时任务。
|
||||
*
|
||||
* 注册到 RuoYi 系统监控-定时任务:
|
||||
* - job name: 招商佣金周结
|
||||
* job group: PROMO
|
||||
* invoke target: commissionSettlementTask.settleWeeklyCommission()
|
||||
* cron: 0 30 0 ? * MON (每周一 00:30)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("commissionSettlementTask")
|
||||
public class CommissionSettlementTask {
|
||||
|
||||
@Autowired
|
||||
private PromoTurnoverMapper promoTurnoverMapper;
|
||||
|
||||
@Autowired
|
||||
private TtUserService userService;
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
/**
|
||||
* 执行每周佣金结算。
|
||||
* 结算周期:上周一 00:00 ~ 本周一 00:00
|
||||
*
|
||||
* 每个招商账号在独立事务中结算:结算记录插入与佣金入账同成败,
|
||||
* 入账失败时回滚该用户的记录,下次任务可重试(幂等检查不会误跳),
|
||||
* 同时避免单个用户异常影响其他用户结算。
|
||||
*/
|
||||
public void settleWeeklyCommission() {
|
||||
log.info("[招商佣金周结] 任务开始 ----------");
|
||||
|
||||
Date[] weekRange = getLastWeekRange();
|
||||
Date startTime = weekRange[0];
|
||||
Date endTime = weekRange[1];
|
||||
Date now = new Date();
|
||||
|
||||
log.info("[招商佣金周结] 结算周期: {} ~ {}",
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", startTime),
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", endTime));
|
||||
|
||||
List<TtUser> businessUsers = promoTurnoverMapper.getBusinessUsers();
|
||||
if (CollectionUtils.isEmpty(businessUsers)) {
|
||||
log.info("[招商佣金周结] 没有招商账号,任务结束");
|
||||
return;
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
|
||||
TransactionTemplate txTemplate = new TransactionTemplate(transactionManager);
|
||||
for (TtUser businessUser : businessUsers) {
|
||||
try {
|
||||
// 每个用户独立事务:失败仅回滚自己,不影响其他用户,且可在下次任务重试
|
||||
Boolean settled = txTemplate.execute(status ->
|
||||
settleSingleBusinessUser(businessUser, startTime, endTime, now));
|
||||
if (Boolean.TRUE.equals(settled)) {
|
||||
successCount++;
|
||||
} else {
|
||||
skipCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[招商佣金周结] 用户ID={} 结算异常,已回滚本用户结算", businessUser.getUserId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[招商佣金周结] 任务完成,成功={}, 跳过={} ----------", successCount, skipCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 结算单个招商账号。
|
||||
*
|
||||
* @return true 本次产生新结算;false 因幂等已存在而跳过
|
||||
*/
|
||||
private boolean settleSingleBusinessUser(TtUser businessUser, Date startTime, Date endTime, Date now) {
|
||||
Integer userId = businessUser.getUserId();
|
||||
|
||||
// 幂等:同一周期已结算则跳过
|
||||
int existed = promoTurnoverMapper.countCommissionRecord(userId, startTime);
|
||||
if (existed > 0) {
|
||||
log.info("[招商佣金周结] userId={} 本周期已结算,跳过", userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
BigDecimal commission = BigDecimal.ZERO;
|
||||
BigDecimal totalFlow = BigDecimal.ZERO;
|
||||
|
||||
// 按 business_id 快照统计上周流水:改绑/解绑不影响已发生流水的招商归属。
|
||||
Map<String, Object> data = promoTurnoverMapper.getWeeklyCommissionDataWithSnapshot(
|
||||
userId, startTime, endTime);
|
||||
BigDecimal recharge = toBd(data, "recharge");
|
||||
BigDecimal newGameConsume = toBd(data, "newGameConsume");
|
||||
BigDecimal classicGameConsume = toBd(data, "classicGameConsume");
|
||||
|
||||
BigDecimal commissionRate = businessUser.getCommissionRate() != null
|
||||
? businessUser.getCommissionRate() : BigDecimal.ZERO;
|
||||
BigDecimal newGameRate = businessUser.getNewGameCommissionRate() != null
|
||||
? businessUser.getNewGameCommissionRate() : BigDecimal.ZERO;
|
||||
BigDecimal classicGameRate = businessUser.getClassicGameCommissionRate() != null
|
||||
? businessUser.getClassicGameCommissionRate() : BigDecimal.ZERO;
|
||||
|
||||
BigDecimal rechargeCommission = calcRate(recharge, commissionRate);
|
||||
BigDecimal newGameCommission = calcRate(newGameConsume, newGameRate);
|
||||
BigDecimal classicGameCommission = calcRate(classicGameConsume, classicGameRate);
|
||||
|
||||
commission = rechargeCommission.add(newGameCommission).add(classicGameCommission);
|
||||
totalFlow = recharge.add(newGameConsume).add(classicGameConsume);
|
||||
|
||||
// 保存结算记录
|
||||
TtCommissionRecord record = new TtCommissionRecord();
|
||||
record.setUserId(userId);
|
||||
record.setCommission(commission);
|
||||
record.setSummaryTime(now);
|
||||
record.setSettlementStartTime(startTime);
|
||||
record.setSettlementEndTime(endTime);
|
||||
record.setTotalFlow(totalFlow);
|
||||
record.setRate(calcCompositeRate(commission, totalFlow));
|
||||
record.setClaimStatus("1");
|
||||
record.setClaimTime(now);
|
||||
promoTurnoverMapper.insertCommissionRecord(record);
|
||||
|
||||
// 佣金到账(入账失败必须抛异常回滚结算记录,否则记录存在但钱未到账,
|
||||
// 且幂等检查会导致永不补发)
|
||||
if (commission.compareTo(BigDecimal.ZERO) > 0) {
|
||||
R<?> credited = userService.updateUserAccount(userId, commission, TtAccountRecordSource.BUSINESS_WEEKLY_COMMISSION);
|
||||
if (R.isError(credited)) {
|
||||
throw new RuntimeException("佣金入账失败: "
|
||||
+ (credited.getMsg() != null ? credited.getMsg() : "未知原因"));
|
||||
}
|
||||
|
||||
// 发送站内信通知
|
||||
String title = MessageCategoryEnum.SYS_PROMO_REBATE.getTitle();
|
||||
String content = String.format(
|
||||
"上周(%s ~ %s)招商周结返利已到账,返利 %s,请注意查收。",
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", startTime),
|
||||
DateUtils.parseDateToStr("yyyy-MM-dd", endTime),
|
||||
commission.toPlainString());
|
||||
try {
|
||||
messageService.sendNotice(Long.valueOf(userId), title, content, MessageCategoryEnum.SYS_PROMO_REBATE.getCode());
|
||||
} catch (Exception e) {
|
||||
log.error("[招商佣金周结] userId={} 发送到账通知失败", userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[招商佣金周结] userId={} 结算完成,流水={}, 佣金={}", userId, totalFlow, commission);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算上周起止时间:上周一 00:00 ~ 本周一 00:00
|
||||
*/
|
||||
private Date[] getLastWeekRange() {
|
||||
Calendar end = Calendar.getInstance();
|
||||
resetTime(end);
|
||||
// 回到本周一 00:00
|
||||
int dayOfWeek = end.get(Calendar.DAY_OF_WEEK);
|
||||
int daysSinceMonday = (dayOfWeek + 5) % 7;
|
||||
end.add(Calendar.DAY_OF_MONTH, -daysSinceMonday);
|
||||
|
||||
Calendar start = (Calendar) end.clone();
|
||||
start.add(Calendar.DAY_OF_MONTH, -7);
|
||||
|
||||
return new Date[]{start.getTime(), end.getTime()};
|
||||
}
|
||||
|
||||
private void resetTime(Calendar calendar) {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按百分比比例计算佣金:amount × rate / 100,setScale(2, HALF_UP)。
|
||||
*/
|
||||
private BigDecimal calcRate(BigDecimal amount, BigDecimal rate) {
|
||||
if (amount == null || rate == null
|
||||
|| amount.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| rate.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return amount.multiply(rate)
|
||||
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算综合费率:commission / totalFlow * 100
|
||||
*/
|
||||
private BigDecimal calcCompositeRate(BigDecimal commission, BigDecimal totalFlow) {
|
||||
if (totalFlow == null || totalFlow.compareTo(BigDecimal.ZERO) <= 0
|
||||
|| commission == null || commission.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
return commission.divide(totalFlow, 6, RoundingMode.HALF_UP)
|
||||
.multiply(BigDecimal.valueOf(100));
|
||||
}
|
||||
|
||||
/** 从 Map 中安全取 BigDecimal,null 返回 ZERO */
|
||||
private BigDecimal toBd(Map<String, Object> map, String key) {
|
||||
if (map == null) return BigDecimal.ZERO;
|
||||
Object v = map.get(key);
|
||||
return v instanceof BigDecimal ? (BigDecimal) v : BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ruoyi.promo.task;
|
||||
|
||||
import com.ruoyi.promo.domain.entity.PromoCdkey;
|
||||
import com.ruoyi.promo.service.PromoCdkeyService;
|
||||
import com.ruoyi.message.service.MessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* CDK 过期/兑完 精确退还定时任务。
|
||||
*
|
||||
* 口径:CDK创建时按 amountMax×num 等额预扣主播金币+积分,兑换时仅随机派发 min~max 金币。
|
||||
* CDK结束(过期/兑完)后,按「预扣总额 - 实际已派发金币总额」精确退还主播金币与积分(同额)。
|
||||
*
|
||||
* 注册到 RuoYi 系统监控-定时任务:
|
||||
* - job name: CDK结束退还
|
||||
* job group: PROMO
|
||||
* invoke target: promoCdkeyRefundTask.settleEndedCdkeyRefunds()
|
||||
* cron: 0 0/10 * * * ? (每10分钟)
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("promoCdkeyRefundTask")
|
||||
public class PromoCdkeyRefundTask {
|
||||
|
||||
@Autowired
|
||||
private PromoCdkeyService promoCdkeyService;
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
/**
|
||||
* 扫描已结束(过期/兑完)且未退还的CDK,逐个精确退还,并按主播聚合发送到账通知。
|
||||
*/
|
||||
public void settleEndedCdkeyRefunds() {
|
||||
log.info("[CDK结束退还] 任务开始 ----------");
|
||||
|
||||
// 1. 全局补齐过期状态(validity 到期且仍进行中的置为已结束)
|
||||
int expired = promoCdkeyService.markExpiredCdkeys();
|
||||
log.info("[CDK结束退还] 补齐过期状态 {} 条", expired);
|
||||
|
||||
// 2. 查询待退还候选:status=1 且 refund_time IS NULL
|
||||
List<PromoCdkey> candidates = promoCdkeyService.selectRefundableCdkeys();
|
||||
if (CollectionUtils.isEmpty(candidates)) {
|
||||
log.info("[CDK结束退还] 无待退还CDK,任务结束");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 逐个退还(每个CDK独立事务),按创建者聚合本轮退还总额与数量
|
||||
Map<Long, BigDecimal> refundByCreator = new HashMap<>();
|
||||
Map<Long, Integer> countByCreator = new HashMap<>();
|
||||
int success = 0;
|
||||
for (PromoCdkey cdkey : candidates) {
|
||||
try {
|
||||
BigDecimal refund = promoCdkeyService.refundOne(cdkey);
|
||||
Long creatorId = cdkey.getCreatorId();
|
||||
if (refund != null && refund.compareTo(BigDecimal.ZERO) > 0) {
|
||||
refundByCreator.merge(creatorId, refund, BigDecimal::add);
|
||||
countByCreator.merge(creatorId, 1, Integer::sum);
|
||||
}
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
log.error("[CDK结束退还] cdkeyId={} 退还异常", cdkey.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 按主播聚合发送站内信(仅对有实际退还金额的主播,避免刷屏)
|
||||
// for (Map.Entry<Long, BigDecimal> entry : refundByCreator.entrySet()) {
|
||||
// Long creatorId = entry.getKey();
|
||||
// BigDecimal total = entry.getValue();
|
||||
// int cnt = countByCreator.getOrDefault(creatorId, 0);
|
||||
// String title = "CDKEY结束退还到账通知";
|
||||
// String content = String.format(
|
||||
// "您有 %d 个CDKEY已结束(过期或兑完),共返还金币 %s、积分 %s,请注意查收。",
|
||||
// cnt, total.toPlainString(), total.toPlainString());
|
||||
// try {
|
||||
// messageService.sendNotice(creatorId, title, content);
|
||||
// } catch (Exception e) {
|
||||
// log.error("[CDK结束退还] creatorId={} 发送退还通知失败", creatorId, e);
|
||||
// }
|
||||
// }
|
||||
|
||||
log.info("[CDK结束退还] 任务完成,处理 {} 条,退还主播 {} 个 ----------",
|
||||
success, refundByCreator.size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.promo.task;
|
||||
|
||||
import com.ruoyi.domain.entity.sys.TtUser;
|
||||
import com.ruoyi.promo.domain.dto.TotalUserExpenditureDTO;
|
||||
import com.ruoyi.promo.mapper.PromoTurnoverMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component("PromoTask")
|
||||
public class PromoTask {
|
||||
|
||||
@Autowired
|
||||
private PromoTurnoverMapper promoTurnoverMapper;
|
||||
|
||||
private Map<Integer, BigDecimal> commissionMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 计算上月佣金
|
||||
*/
|
||||
public void calculateLastMonthCommission() {
|
||||
// 汇总所有用户上个月的充值总额
|
||||
List<TotalUserExpenditureDTO> lastMonthTotalUserExpenditure = promoTurnoverMapper.getLastMonthTotalUserExpenditure();
|
||||
for (TotalUserExpenditureDTO totalUserExpenditureDTO : lastMonthTotalUserExpenditure) {
|
||||
// 向上递归给各级代理分配佣金
|
||||
if (!Objects.isNull(totalUserExpenditureDTO.getParentId())) {
|
||||
distributeCommission(totalUserExpenditureDTO.getParentId(),
|
||||
totalUserExpenditureDTO.getCommissionRate(),
|
||||
totalUserExpenditureDTO.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配佣金
|
||||
*/
|
||||
private void distributeCommission(Integer parentId,
|
||||
BigDecimal commissionRate,
|
||||
BigDecimal amount) {
|
||||
TtUser ttUser = promoTurnoverMapper.findById(parentId);
|
||||
BigDecimal parentCommissionRate = ttUser.getCommissionRate();
|
||||
BigDecimal commission = parentCommissionRate.multiply(amount).subtract(commissionRate.multiply(amount));
|
||||
System.err.println("代理:" + parentId + ",佣金比例:" + parentCommissionRate + ",获得佣金:" + commission);
|
||||
// 计算并存储该级代理的佣金
|
||||
// 将相同ID的代理佣金相加
|
||||
// 判断是否还有上级代理,有继续递归
|
||||
if (!Objects.isNull(ttUser.getParentId())) {
|
||||
distributeCommission(ttUser.getParentId(),
|
||||
ttUser.getCommissionRate(),
|
||||
amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user