first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>skins-service</artifactId>
<version>4.8.2</version>
</parent>
<artifactId>service-user</artifactId>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>service-admin</artifactId>
<version>4.8.2</version>
</dependency>
<!-- 消息通知统一发送模块 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>message-service</artifactId>
<version>4.8.2</version>
</dependency>
<!-- 滑块行为验证码 -->
<dependency>
<groupId>cloud.tianai.captcha</groupId>
<artifactId>tianai-captcha-springboot-starter</artifactId>
<version>1.5.5</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>service-thirdparty</artifactId>
<version>4.8.2</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,69 @@
package com.ruoyi.user.config;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.resource.ResourceStore;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
@Slf4j
@Configuration
public class CaptchaResourceConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Bean
public ResourceStore resourceStore() throws IOException {
LocalMemoryResourceStore resourceStore = new LocalMemoryResourceStore();
// 动态扫描 classpath:image/ 下的背景图
// 优先加载 jpg(体积更小、I/O 更快,tianai-captcha 官方推荐),
// 仅在没有 jpg 时才回退加载 png,避免两种格式重复注册
// 注意:必须用 classpath*: 前缀,否则打成 fat jar 后部署会失败
// (FileNotFoundException: class path resource [image/] cannot be resolved to URL)。
// PathMatchingResourcePatternResolver 只有在 classpath*: 下才会展开 jar 内部的 entries。
int count = registerImages("classpath*:image/*.jpg", resourceStore);
if (count == 0) {
log.info("[滑块验证码] 未找到 JPG,回退加载 PNG");
count = registerImages("classpath*:image/*.png", resourceStore);
}
log.info("[滑块验证码] 已加载 {} 张背景图片", count);
if (count == 0) {
log.error("[滑块验证码] 未扫到任何背景图片!请检查:\n" +
" 1) service-user/src/main/resources/image/ 是否存在且含 PNG/JPG\n" +
" 2) 文件名是否为 ASCII(避免中文/空格)\n" +
" 3) fat jar 部署时内嵌 jar 中是否含 image/ 目录");
}
return resourceStore;
}
/**
* 扫描指定模式的图片并注册到 ResourceStore
*
* @param pattern Spring 资源匹配模式,如 classpath*:image/*.jpg
* @param resourceStore 资源存储器
* @return 注册的图片数量
*/
private int registerImages(String pattern, LocalMemoryResourceStore resourceStore) throws IOException {
org.springframework.core.io.Resource[] images = applicationContext.getResources(pattern);
int count = 0;
for (org.springframework.core.io.Resource img : images) {
String filename = img.getFilename();
if (filename != null) {
resourceStore.addResource(CaptchaTypeConstant.SLIDER,
new Resource("classpath", "image/" + filename, "default"));
count++;
}
}
return count;
}
}
@@ -0,0 +1,44 @@
package com.ruoyi.user.consumer;
import com.rabbitmq.client.Channel;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.domain.other.TtNotice;
import com.ruoyi.user.service.ApiNoticeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
@Component
@Slf4j
public class NoticeConsumer {
@Autowired
private ApiNoticeService apiNoticeService;
@RabbitListener(queues = "notice_queue")
public void insertNotice(Map<String, String> noticeMap, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
String userId = noticeMap.get("userId");
String title = noticeMap.get("title");
String content = noticeMap.get("content");
String createTime = noticeMap.get("createTime");
TtNotice ttNotice = new TtNotice();
ttNotice.setUserId(Long.valueOf(userId));
ttNotice.setTitle(title);
ttNotice.setContent(content);
ttNotice.setCreateTime(DateUtils.parseDate(createTime));
apiNoticeService.addNotice(ttNotice);
try {
//手动确认消费
channel.basicAck(tag, false);
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class AmountRankRequest {
private Integer type;
private Integer page;
private Integer size;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class BaiduCallbackRequest {
/** 类型 */
private Integer newType;
/** 百度ID */
private String bdVid;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class BatchOperationMessageRequest {
private Integer[] ids;
private String status;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.contract.api.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class BindInvitationCodeRequest {
@NotBlank(message = "邀请码不能为空")
private String invitationCode;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class ContentByTypeRequest {
private String alias;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class CreditsRankRequest {
private Integer type;
private Integer page;
private Integer size;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
import java.util.List;
@Data
public class ExchangeOrnamentsId {
private List<Long> ornamentsIds;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetAdvertisementRequest {
/** 广告ID */
private Integer id;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetAnnouncementListRequest {
/** Tab 类型:0 系统 / 1 游戏;为空返回全部 */
private Integer tabType;
}
@@ -0,0 +1,15 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetAnnouncementRequest {
/**
* 消息主键ID,与 /list 返回结构中的 id 字段对齐。
* 需配合 source 使用:source=1 时为公告ID,source=2 时为通知ID。
*/
private Integer id;
/** 消息来源:1 公告 / 2 通知,与 /list 返回结构中的 source 字段对齐 */
private Integer source;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetMessageListRequest {
/** 消息类型ID*/
private Integer id;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetNoticeRequest {
/** 通知ID */
private Integer noticeId;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetRewardRequest {
/** 任务ID */
private Integer taskId;
}
@@ -0,0 +1,8 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class GetShoppingValueRequest {
private String value;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class IntegratingConversionRequest {
private BigDecimal credits;
}
@@ -0,0 +1,26 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
/**
* 批量设置通知为已读请求
*/
@Data
public class MarkAsReadRequest {
/**
* 分类类型:0-系统通知,1-游戏通知
*/
private Integer categoryType;
/**
* 获取分类前缀(根据前端传入的类型进行映射)
* @return 分类前缀字符串
*/
public String getCategoryPrefix() {
if (categoryType == null) {
return null;
}
// 0: SYS_, 1: GAME_
return categoryType == 1 ? "GAME_" : "SYS_";
}
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class MessageRequest {
private Integer id;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class PWelfareCreditsRecordsRequest {
private Integer type;
private Integer page;
private Integer size;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class PWelfareRecordsRequest {
private Integer page;
private Integer size;
}
@@ -0,0 +1,28 @@
package com.ruoyi.user.contract.api.request;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class PersonalRecordRequest {
@NotNull
@Min(value = 1,message = "最小值1")
private Integer page;
@NotNull
@Min(value = 1,message = "最小值1")
@Max(value = 50, message = "最大值20")
private Integer pageSize;
// 1:今日 2:昨日 3:最近7天
@NotNull(message = "type不能为空")
private Integer type;
// 查询的数据源 1:充值 2:消费 3:红包
@NotNull(message = "source不能为空")
private Integer source;
// 过滤选项 筛选条件 0:支出 1:收入
private Integer filteringOptions;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class PropRankOfDayRequest {
private Integer type;
private Integer number;
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class QueryTransferRecordRequest {
private String kind;
private Integer pageNum;
private Integer pageSize;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class ReceiveRedPacketRequest {
/** 红包码 */
private String code;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class RechargeDetailsRequest {
/** 页码 */
private Integer page;
/** 每页数量 */
private Integer size;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class UserCreditsToAmountRequest {
/** 积分数量 */
private BigDecimal credits;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class UserRechargeBonusRequest {
/** 充值金额门槛 */
private Integer recharge;
/** 奖励金额 */
private Double bonus;
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
import java.util.List;
@Data
public class UserVipLevelRequest {
/**
* 要领取的 VIP 显示等级集合(VIPn 中的 n,支持一次领多个,例如 [1,2,3] 表示 VIP1、VIP2、VIP3)
*/
private List<Integer> vipLevels;
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.contract.api.request;
import lombok.Data;
@Data
public class ViewMessageRequest {
/** 消息ID */
private Integer id;
}
@@ -0,0 +1,48 @@
package com.ruoyi.user.contract.api.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 巅峰时刻 - 近7日高品质TOP10记录
*/
@Data
public class PeakMomentResponse {
/** 用户ID */
private Integer userId;
/** 用户昵称 */
private String nickName;
/** 用户头像 */
private String avatar;
/** 近7日内获得的高品质TOP10饰品列表(按价格降序) */
private List<PeakItem> items;
@Data
public static class PeakItem {
/** 记录ID */
private Long id;
/** 饰品完整名称(tt_ornament.name,回退 tt_box_records.ornament_name) */
private String name;
/** 饰品名称 */
private String ornamentName;
/** 饰品图片 */
private String imageUrl;
/** 饰品等级图片 */
private String ornamentLevelImg;
/** 外观名称(如:崭新出厂) */
private String exteriorName;
/** 饰品价格 */
private BigDecimal ornamentsPrice;
/** 饰品来源 */
private Integer source;
/** 获得时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
}
@@ -0,0 +1,24 @@
package com.ruoyi.user.contract.api.response;
import lombok.Data;
import java.util.List;
@Data
public class PersonalRecordResponse {
private long total;
private List<RecordVO> rows;
@Data
public static class RecordVO {
private String amount;
private String finalAmount;
private String remark;
private String datetime;
private Integer source;
}
}
@@ -0,0 +1,12 @@
package com.ruoyi.user.contract.api.response;
import com.ruoyi.user.model.vo.ApiTransferRecordVO;
import lombok.Data;
import java.util.List;
@Data
public class QueryTransferRecordResponse {
private long total;
private List<ApiTransferRecordVO> rows;
}
@@ -0,0 +1,41 @@
package com.ruoyi.user.contract.api.response;
import com.ruoyi.domain.other.TtVipLevel;
import lombok.Data;
import java.util.List;
@Data
public class QueryVipRewardResponse {
/** 今日游戏流水 */
private String todayAmount;
/** 预计奖励(今日流水 × 当前VIP等级流水返利百分比) */
private String todayReward;
/** 昨日游戏流水 */
private String yesterdayAmount;
/** 已领奖励(昨日流水对应的VIP流水返利) */
private String yesterdayReward;
/** VIP等级列表(含每个等级的达标金额、流水返利百分比等) */
private List<TtVipLevel> vipLevels;
// ====== 当前等级与经验进度 ======
/** 当前VIP等级 */
private Integer currentVipLevel;
/** 当前累计经验值 */
private Long currentExp;
/** 下一级所需经验阈值 */
private Long nextLevelExp;
/** 距离下一级还需经验值 */
private Long needExp;
/** 是否已是最高VIP等级 */
private Boolean maxLevel;
/** 进度文本,形如 "3500/12000" */
private String progressText;
/** 当前VIP每日转赠次数上限 */
private Integer transferCount;
/** 今日还可转赠次数 */
private Integer availableTransferCount;
}
@@ -0,0 +1,70 @@
package com.ruoyi.user.contract.api.response;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.List;
/**
* VIP 流水奖励接口响应(精简版,仅包含流水奖励页面所需数据)
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VipFlowRewardResponse {
/** 今日游戏流水 */
private String todayAmount;
/** 预计奖励(今日流水 × 当前VIP等级流水返利百分比) */
private String todayReward;
/** 昨日游戏流水 */
private String yesterdayAmount;
/** 已领奖励(昨日VIP流水返利实际发放金额) */
private String yesterdayReward;
/** VIP等级列表(精简:仅名称、达标金额、流水返利百分比) */
private List<VipLevelItem> vipLevels;
/** 当前VIP等级 */
private Integer currentVipLevel;
/** 当前累计经验值 */
private Long currentExp;
/** 下一级所需经验阈值 */
private Long nextLevelExp;
/** 距离下一级还需经验值 */
private Long needExp;
/** 是否已是最高VIP等级 */
private Boolean maxLevel;
/** 进度文本,形如 "3500/12000" */
private String progressText;
/**
* VIP等级精简项
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class VipLevelItem {
/** 等级名称(如 VIP0, VIP1 ...) */
private String name;
/** 等级达标金额(经验阈值) */
private BigDecimal rechargeThreshold;
/** 流水返利百分比(如 0.2 代表 0.2%) */
private BigDecimal flowRebatePercent;
/** 当前等级进度文本,形如 "500/500"、"3500/5000" */
private String progressText;
}
}
@@ -0,0 +1,28 @@
package com.ruoyi.user.controller;
import com.ruoyi.admin.service.TtAdvertisementService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.other.TtAdvertisement;
import com.ruoyi.user.contract.api.request.GetAdvertisementRequest;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
@Api(tags = "广告查询")
@RestController
@RequestMapping("/api/advertisement")
public class ApiAdvertisementController {
private final TtAdvertisementService ttAdvertisementService;
public ApiAdvertisementController(TtAdvertisementService ttAdvertisementService) {
this.ttAdvertisementService = ttAdvertisementService;
}
@PostMapping("/getInfo")
public R<TtAdvertisement> getInfo(@RequestBody GetAdvertisementRequest getAdvertisementRequest) {
TtAdvertisement ttAdvertisement = ttAdvertisementService.getById(getAdvertisementRequest.getId());
// ttBanner.setPicture("");
return R.ok(ttAdvertisement);
}
}
@@ -0,0 +1,171 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.domain.other.TtAnnouncement;
import com.ruoyi.user.contract.api.request.GetAnnouncementListRequest;
import com.ruoyi.user.contract.api.request.GetAnnouncementRequest;
import com.ruoyi.user.contract.api.request.MarkAsReadRequest;
import com.ruoyi.user.model.vo.ApiNoticeVO;
import com.ruoyi.user.model.vo.MessageItemVO;
import com.ruoyi.user.service.ApiAnnouncementService;
import com.ruoyi.user.service.ApiNoticeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Api(tags = "公告")
@RestController
@RequestMapping("/api/announcement")
public class ApiAnnouncementController extends BaseController {
@Autowired
private ApiAnnouncementService apiAnnouncementService;
@Autowired
private ApiNoticeService apiNoticeService;
@ApiOperation("获取公告列表(含用户通知,统一结构返回)")
@ApiResponse(code = 200, message = "read【阅读状态(0未读 1已读)】")
@PostMapping("/list")
public AjaxResult list(@RequestBody(required = false) GetAnnouncementListRequest request) {
Integer tabType = request == null ? null : request.getTabType();
Long userId = SecurityUtils.getUserId();
List<MessageItemVO> result = new ArrayList<>();
// tabType=1(游戏 Tab)只返回通知;tabType=0(系统 Tab)或 null 才返回公告
if (!Integer.valueOf(1).equals(tabType)) {
List<TtAnnouncement> announcementList = apiAnnouncementService.getAnnouncementList(userId);
if (announcementList != null) {
for (TtAnnouncement a : announcementList) {
result.add(toItem(a));
}
}
}
// 根据 tabType 决定查询哪类通知
List<ApiNoticeVO> noticeList;
if (tabType == null) {
noticeList = apiNoticeService.getNoticeList(userId);
} else if (tabType == 1) {
noticeList = apiNoticeService.getNoticeListByCategoryPrefix(userId, "GAME_");
} else {
noticeList = apiNoticeService.getNoticeListByCategoryPrefix(userId, "SYS_");
}
if (noticeList != null) {
for (ApiNoticeVO n : noticeList) {
result.add(toItem(n));
}
}
// 按创建时间倒序
result.sort(Comparator.comparing(MessageItemVO::getCreateTime,
Comparator.nullsLast(Comparator.reverseOrder())));
return AjaxResult.success(result);
}
@ApiOperation("获取系统公告列表(免登录)")
@PostMapping("/systemList")
public TableDataInfo systemList() {
startPage();
List<TtAnnouncement> announcementList = apiAnnouncementService.getSystemAnnouncementList();
return getDataTable(announcementList);
}
@ApiOperation("获取公告/通知详情(统一结构返回)")
@PostMapping("/detail")
public AjaxResult getAnnouncementByAnnouncementId(@RequestBody GetAnnouncementRequest request) {
Long userId = SecurityUtils.getUserId();
if (request.getId() == null || request.getSource() == null) {
return AjaxResult.error("请传入 id 与 source");
}
// source=1 公告:返回公告详情同时写入 tt_announcement_read
if (Integer.valueOf(1).equals(request.getSource())) {
TtAnnouncement ttAnnouncement = apiAnnouncementService
.getAnnouncementByAnnouncementId(request.getId(), userId);
if (ttAnnouncement == null) {
return AjaxResult.error("公告不存在");
}
// tt_announcement 表本身没有 read 列,read 仅在 /list 的 SQL 里通过 LEFT JOIN tt_announcement_read 计算;
// 此处按主键直查不会带回 read。但服务方法内部已写入 tt_announcement_read,调用后对当前用户必然为已读,
// 因此显式置为 "1",与 /list 的语义保持一致。
ttAnnouncement.setRead("1");
return AjaxResult.success(toItem(ttAnnouncement));
}
// source=2 通知:返回通知详情同时自动将 tt_notice.read 置为 1
if (Integer.valueOf(2).equals(request.getSource())) {
ApiNoticeVO noticeVO = apiNoticeService.getNoticeByNoticeId(userId, request.getId());
if (noticeVO == null) {
return AjaxResult.error("公告不存在");
}
return AjaxResult.success(toItem(noticeVO));
}
return AjaxResult.error("source 取值必须为 1(公告)或 2(通知)");
}
/** 公告 → 统一展示项 */
private MessageItemVO toItem(TtAnnouncement a) {
MessageItemVO vo = new MessageItemVO();
vo.setId(a.getAnnouncementId());
vo.setSource(1);
vo.setTitle(a.getTitle());
vo.setContent(a.getContent());
vo.setCategory(a.getCategory());
vo.setTabType(resolveTabType(a.getCategory()));
vo.setRead(a.getRead());
vo.setCreateTime(a.getCreateTime());
return vo;
}
/** 通知 → 统一展示项 */
private MessageItemVO toItem(ApiNoticeVO n) {
MessageItemVO vo = new MessageItemVO();
vo.setId(n.getNoticeId());
vo.setSource(2);
vo.setTitle(n.getTitle());
vo.setContent(n.getContent());
vo.setCategory(n.getCategory());
vo.setTabType(resolveTabType(n.getCategory()));
vo.setRead(n.getRead());
vo.setCreateTime(n.getCreateTime());
return vo;
}
/** 根据 category 前缀推导 Tab 类型:GAME_ 为游戏(1),其他为系统(0) */
private Integer resolveTabType(String category) {
if (category != null && category.toUpperCase().startsWith("GAME_")) {
return 1;
}
return 0;
}
@ApiOperation("获取未读公告数量(含未读通知)")
@PostMapping("/countUnreadAnnouncement")
public AjaxResult countUnreadAnnouncement() {
Long userId = SecurityUtils.getUserId();
int announcementUnread = apiAnnouncementService.countUnreadAnnouncement(userId);
int noticeUnread = apiNoticeService.countUnreadNotice(userId);
return AjaxResult.success(announcementUnread + noticeUnread);
}
@ApiOperation("批量设置指定分类的通知为已读(游戏/系统)")
@PostMapping("/markAsReadByCategory")
public AjaxResult markNoticesAsReadByCategory(@RequestBody MarkAsReadRequest request) {
int result = apiNoticeService.markNoticesAsReadByCategory(request.getCategoryPrefix());
return AjaxResult.success(result);
}
}
@@ -0,0 +1,53 @@
package com.ruoyi.user.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.service.TtUserBlendErcashService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.ChildDetailByBoss;
import com.ruoyi.domain.vo.ChildDetailByBossVO;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiShoppingService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "用户收支记录")
@RestController
@RequestMapping("/api/blendErcash")
public class ApiBlendErcashController extends BaseController {
@Autowired
private TtUserBlendErcashService blendErcashService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private ApiShoppingService apiShoppingService;
@Autowired
private TtUserService userService;
// 下级消费明细
@ApiOperation("粉丝消费明细")
@PostMapping("/childDetailByBoss")
// @Anonymous
public R<Page<ChildDetailByBossVO>> childDetailByBoss(@RequestBody @Validated ChildDetailByBoss param){
Long userId = getUserId();
if (ObjectUtil.isEmpty(userId)) return R.fail(401,"登录过期");
param.setBossId(Integer.valueOf(userId.toString()));
return blendErcashService.childDetailByBoss(param);
}
}
@@ -0,0 +1,67 @@
package com.ruoyi.user.controller;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.contract.api.request.ReceiveRedPacketRequest;
import com.ruoyi.user.contract.api.response.QueryVipRewardResponse;
import com.ruoyi.user.contract.api.response.VipFlowRewardResponse;
import com.ruoyi.user.service.ApiBonusService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
@Api(tags = "福利补贴")
@RestController
@RequestMapping("/api/bonus")
public class ApiBonusController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiBonusService apiBonusService;
private final TtUserService userService;
public ApiBonusController(ISysConfigService sysConfigService,
ApiBonusService apiBonusService,
TtUserService userService) {
this.sysConfigService = sysConfigService;
this.apiBonusService = apiBonusService;
this.userService = userService;
}
@ApiOperation("领取红包")
@UpdateUserCache
@PostMapping("/receiveRedPacket")
public R receiveRedPacket(@RequestBody ReceiveRedPacketRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiBonusService.receiveRedPacket(request.getCode(), ttUser);
}
@ApiOperation("查询vip流水奖励")
@PostMapping("/vipreward/list")
public R<QueryVipRewardResponse> vipRewardList() {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiBonusService.vipRewardList(ttUser);
}
@ApiOperation("VIP流水奖励(精简版)")
@PostMapping("/vipFlowReward")
public R<VipFlowRewardResponse> vipFlowReward() {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiBonusService.vipFlowReward(ttUser);
}
}
@@ -0,0 +1,99 @@
package com.ruoyi.user.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.AjaxResult;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.other.ApiCaptchaCheckRequest;
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;
/**
* 滑块验证码 Controller
* POST /api/captcha/gen - 生成验证码
* POST /api/captcha/check - 校验验证码,通过后在 Redis 写入标记供后续短信/密码登录使用
*/
@Api(tags = "滑块验证码")
@Slf4j
@RestController
@RequestMapping("/api")
public class ApiCaptchaController extends BaseController {
@Autowired
private ImageCaptchaApplication application;
@Autowired
private RedisCache redisCache;
/** 滑块位置容差,0.3 表示允许 30% 偏差 */
private static final float SLIDER_TOLERANT = 0.05f;
/**
* 生成滑块验证码
*/
@ApiOperation("生成滑块验证码")
@Anonymous
@PostMapping("/captcha/gen")
public AjaxResult 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 AjaxResult.error("验证码生成失败");
}
return AjaxResult.success(response.getData());
}
/**
* 校验滑块验证码
* 通过后在 Redis 写入 captcha:verified:{phoneNumber} = captchaId,有效期 5 分钟
*/
@ApiOperation("校验滑块验证码")
@Anonymous
@PostMapping("/captcha/check")
public AjaxResult checkCaptcha(@RequestBody ApiCaptchaCheckRequest request) {
if (StringUtils.isEmpty(request.getId())) {
return AjaxResult.error("验证码ID不能为空");
}
if (StringUtils.isEmpty(request.getPhoneNumber())) {
return AjaxResult.error("手机号不能为空");
}
ApiResponse<?> checkResponse = application.matching(request.getId(), request.getData());
if (!checkResponse.isSuccess()) {
log.warn("[滑块验证] 验证失败, captchaId={}, phone={}", request.getId(), request.getPhoneNumber());
return AjaxResult.error("滑块验证失败,请重试");
}
// 验证通过,写入 Redis 标记,5 分钟内可用于发短信或密码登录
String verifyKey = "captcha:verified:" + request.getPhoneNumber();
redisCache.setCacheObject(verifyKey, request.getId(), 5, TimeUnit.MINUTES);
return AjaxResult.success("验证通过");
}
}
@@ -0,0 +1,216 @@
package com.ruoyi.user.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.mapper.TtPromotionUpdateMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.entity.sys.TtPromotionUpdate;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.NewUserInfo;
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.utils.StringUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiLoginBody;
import com.ruoyi.domain.other.ApiVerificationCodeLoginBody;
import com.ruoyi.domain.other.ApiVerificationCodeLoginOrRegisterBody;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiLoginService;
import com.ruoyi.user.service.ApiUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 登录Controller
*/
@Api(tags = "客户端用户认证")
@RestController
@RequestMapping("/api")
public class ApiLoginController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiLoginService loginService;
private final TokenService tokenService;
@Autowired
private TtUserMapper userMapper;
public ApiLoginController(ISysConfigService sysConfigService,
ApiLoginService loginService,
TokenService tokenService) {
this.sysConfigService = sysConfigService;
this.loginService = loginService;
this.tokenService = tokenService;
}
@Autowired
private ApiUserService apiUserService;
@Autowired
private TtUserService userService;
@Autowired
private TtPromotionUpdateMapper ttPromotionUpdateMapper;
@ApiOperation("客户端用户登录")
@Anonymous
@PostMapping("/login")
public AjaxResult login(@RequestBody @Validated ApiLoginBody loginBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return AjaxResult.error("网站维护中......");
}
try {
var loginUser = getLoginUser();
if (Objects.nonNull(loginUser)) {
String userName = loginUser.getUsername();
tokenService.delLoginUser(loginUser.getToken());
AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + userName, Constants.LOGOUT, "退出成功"));
}
} catch (Exception e) {
}
AjaxResult ajax = AjaxResult.success();
Pair<String, AjaxResult> pair = loginService.login(loginBody.getUsername(), loginBody.getPassword());
if (pair.getValue() != null) {
return pair.getValue();
}
ajax.put(Constants.TOKEN, pair.getKey());
return ajax;
}
@ApiOperation("客户端用户验证码登录")
@Anonymous
@PostMapping("/verificationCodeLogin")
public AjaxResult verificationCodeLogin(@RequestBody @Validated ApiVerificationCodeLoginBody apiVerificationCodeLoginBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return AjaxResult.error("网站维护中......");
}
return loginService.verificationCodeLogin(apiVerificationCodeLoginBody.getPhoneNumber(), apiVerificationCodeLoginBody.getCode());
}
@ApiOperation("客户端用户验证码登录或注册")
@Anonymous
@PostMapping("/verificationCodeLoginOrRegister")
public AjaxResult verificationCodeLoginOrRegister(@RequestBody @Validated ApiVerificationCodeLoginOrRegisterBody req) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return AjaxResult.error("网站维护中......");
}
return loginService.verificationCodeLoginOrRegister(req.getPhoneNumber(), req.getCode(), req.getParentInvitationCode());
}
@ApiOperation("获取用户信息")
@NewUserInfo
@PostMapping("/getInfo")
public R<UserData> getInfo() {
UserData userData = getLoginUser().getUserData();
TtUser ttUser = userService.getById(getUserId());
apiUserService.authenticationOk(ttUser);
if (!"1".equals(userData.getIsRealCheck())) {
userData.setRealName("");
}
userData.setAccountAmount(ttUser.getAccountAmount());
userData.setAccountCredits(ttUser.getAccountCredits());
userData.setDeliveryAddress(ttUser.getDeliveryAddress());
// 获取下级总人数
userData.setSubordinateNumber(apiUserService.getSubordinateNumber(getUserId()));
userData.setInvitationCode(ttUser.getInvitationCode());
userData.setVipLevel(convertToDisplayVipLevel(ttUser.getVipLevel()));
return R.ok(userData);
}
@ApiOperation("获取用户余额信息")
@NewUserInfo
@PostMapping("/getAccountAmount")
public R<UserData> getAccountAmount() {
UserData userData = getLoginUser().getUserData();
TtUser ttUser = userService.getById(getUserId());
userData.setAccountAmount(ttUser.getAccountAmount());
userData.setAccountCredits(ttUser.getAccountCredits());
userData.setDeliveryAddress(ttUser.getDeliveryAddress());
userData.setInvitationCode(ttUser.getInvitationCode());
userData.setVipLevel(convertToDisplayVipLevel(ttUser.getVipLevel()));
userData.setPassword("");
return R.ok(userData);
}
private Integer convertToDisplayVipLevel(Integer dbVipLevel) {
return dbVipLevel != null && dbVipLevel > 0 ? dbVipLevel - 1 : 0;
}
@ApiOperation("获取用户信息")
@NewUserInfo
@PostMapping("/getParentInfo")
public R<Map<String, Object>> getParentInfo() {
TtUser ttUser = userService.getById(getUserId());
if (ttUser.getParentId() == null) {
return R.ok();
}
var parent = userService.getById(ttUser.getParentId());
if (parent == null) {
return R.ok();
}
// 推荐时间:优先取 tt_promotion_update.create_time(注册时绑定即落库),
// 没有记录的兜底为当前用户的 createTime(用户的注册时间)。
Date recommendTime = null;
TtPromotionUpdate promotion = ttPromotionUpdateMapper.selectOne(
Wrappers.<TtPromotionUpdate>lambdaQuery()
.eq(TtPromotionUpdate::getEmployeeId, ttUser.getUserId())
.eq(TtPromotionUpdate::getBossId, parent.getUserId())
.orderByAsc(TtPromotionUpdate::getCreateTime)
.last("limit 1"));
if (promotion != null && promotion.getCreateTime() != null) {
Timestamp ts = promotion.getCreateTime();
recommendTime = new Date(ts.getTime());
} else if (ttUser.getCreateTime() != null) {
recommendTime = ttUser.getCreateTime();
}
Map<String, Object> r = new HashMap<>();
r.put("userId", parent.getUserId());
r.put("avatar", parent.getAvatar());
r.put("nickName", parent.getNickName());
r.put("invitationCode", parent.getInvitationCode());
r.put("recommendTime", recommendTime == null
? null
: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(recommendTime));
return R.ok(r);
}
@ApiOperation("客户端用户退出")
@PostMapping("/logout")
public R<Object> logout() {
if (StringUtils.isNotNull(getLoginUser())) {
String userName = getUsername();
tokenService.delLoginUser(getToken());
AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + userName, Constants.LOGOUT, "退出成功"));
}
return R.ok("退出成功!");
}
}
@@ -0,0 +1,67 @@
package com.ruoyi.user.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ruoyi.admin.service.TtMessageService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.other.TtMessage;
import com.ruoyi.user.contract.api.request.BatchOperationMessageRequest;
import com.ruoyi.user.contract.api.request.MessageRequest;
import com.ruoyi.user.service.ApiMessageService;
import com.ruoyi.domain.vo.ApiMessageDataVO;
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.Arrays;
import java.util.List;
@Api(tags = "消息")
@RestController
@RequestMapping("/api/message")
public class ApiMessageController extends BaseController {
private final ApiMessageService apiMessageService;
public ApiMessageController(ApiMessageService apiMessageService) {
this.apiMessageService = apiMessageService;
}
@Autowired
private TtMessageService ttMessageService;
@PostMapping("/list")
public PageDataInfo<TtMessage> list() {
startPage();
QueryWrapper<TtMessage> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByDesc("id");
List<TtMessage> list = ttMessageService.list(queryWrapper);
return getPageData(list);
}
@ApiOperation("获取消息列表")
@PostMapping("/getMessageList")
public PageDataInfo<ApiMessageDataVO> getMessageList(@RequestBody(required = false) MessageRequest request){
startPage();
List<ApiMessageDataVO> list = apiMessageService.getMessageList(getUserId(), request.getId());
return getPageData(list);
}
@ApiOperation("已读Message")
@PostMapping("/view")
public R<ApiMessageDataVO> view(@RequestBody MessageRequest request){
ApiMessageDataVO data = apiMessageService.view(getUserId(), request.getId());
return R.ok(data);
}
@ApiOperation("分批操作Message")
@PostMapping("/batchOperation")
public R<Boolean> batchOperation(@RequestBody BatchOperationMessageRequest request){
String msg = apiMessageService.batchOperation(getUserId(), Arrays.asList(request.getIds()), request.getStatus());
return StringUtils.isEmpty(msg) ? R.ok(true, "操作成功!") : R.fail(false, msg);
}
}
@@ -0,0 +1,57 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.domain.other.TtNotice;
import com.ruoyi.user.contract.api.request.GetNoticeRequest;
import com.ruoyi.user.model.vo.ApiNoticeVO;
import com.ruoyi.user.service.ApiNoticeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
@Api(tags = "通知")
@RestController
@RequestMapping("/api/notice")
public class ApiNoticeController extends BaseController {
@Autowired
private ApiNoticeService apiNoticeService;
@ApiOperation("获取通知列表")
@ApiResponse(code = 200, message = "read【阅读状态(0未读 1已读)】")
@PostMapping("/list")
public TableDataInfo list() {
startPage();
Long userId = SecurityUtils.getUserId();
List<ApiNoticeVO> apiNoticeVOList = apiNoticeService.getNoticeList(userId);
return getDataTable(apiNoticeVOList);
}
@ApiOperation("获取通知详情")
@PostMapping("/detail")
public AjaxResult getNoticeByNoticeId(@RequestBody GetNoticeRequest request) {
Long userId = SecurityUtils.getUserId();
ApiNoticeVO apiNoticeVO = apiNoticeService.getNoticeByNoticeId(userId, request.getNoticeId());
if (Objects.isNull(apiNoticeVO)) {
return AjaxResult.error("通知不存在");
}
return AjaxResult.success(apiNoticeVO);
}
@ApiOperation("获取未读通知数量")
@PostMapping("/countUnreadNotice")
public AjaxResult countUnreadNotice() {
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(apiNoticeService.countUnreadNotice(userId));
}
}
@@ -0,0 +1,30 @@
package com.ruoyi.user.controller;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.admin.service.TtOrderService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.user.contract.api.request.RechargeDetailsRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Api(tags = "订单信息")
@RestController
@RequestMapping("/api/order")
public class ApiOrderController extends BaseController {
@Autowired
private TtOrderService ttOrderService;
@ApiOperation("充值明细")
@PostMapping("/list")
public R list(@RequestBody RechargeDetailsRequest request) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) return R.fail("登录过期。");
return ttOrderService.clientList(request.getPage(), request.getSize(), userId.intValue());
}
}
@@ -0,0 +1,30 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo;
import com.ruoyi.user.service.ApiPromotionRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/promotionRecord")
public class ApiPromotionRecordController extends BaseController {
@Autowired
private ApiPromotionRecordService apiPromotionRecordService;
@PostMapping("/getPromotionRecord")
public PageDataInfo<TtPromotionRecordVo> getPromotionRecord(){
startPage();
Long userId = getUserId();
List<TtPromotionRecordVo> list = apiPromotionRecordService.getPromotionRecord(userId.intValue());
return getPageData(list);
}
}
@@ -0,0 +1,43 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiRegisterService;
import com.ruoyi.domain.other.ApiRegisterBody;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
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;
@Api(tags = "用户注册")
@RestController
@RequestMapping("/api/register")
public class ApiRegisterController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiRegisterService apiRegisterService;
public ApiRegisterController(ISysConfigService sysConfigService,
ApiRegisterService apiRegisterService) {
this.sysConfigService = sysConfigService;
this.apiRegisterService = apiRegisterService;
}
@ApiOperation("用户注册")
@PostMapping
@Anonymous
public R<Object> register(@RequestBody @Validated ApiRegisterBody registerBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String msg = apiRegisterService.register(registerBody);
return StringUtils.isEmpty(msg) ? R.ok("注册成功!") : R.fail(msg);
}
}
@@ -0,0 +1,47 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.share.ShareRecordQueryDTO;
import com.ruoyi.domain.vo.share.ShareCenterVO;
import com.ruoyi.domain.vo.share.ShareRecordPageVO;
import com.ruoyi.user.service.ApiShareService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
/**
* 客户端 - 主页"游戏分享"模块
*/
@Api(tags = "客户端-游戏分享")
@RestController
@RequestMapping("/api/share")
public class ApiShareController extends BaseController {
@Autowired
private ApiShareService apiShareService;
/**
* 分享中心顶部数据:邀请码、今日收益、累计收益、邀请人数、分享链接
*/
@ApiOperation("获取分享中心顶部数据")
@PostMapping("/center")
public R<ShareCenterVO> getCenter() {
return R.ok(apiShareService.getShareCenter(getUserId()));
}
/**
* 分享记录列表(支持按下级用户ID搜索)
*
* @param query 查询参数:subUserId(可选)、pageNum、pageSize(均由前端传入)
*/
@ApiOperation("查询分享记录列表")
@PostMapping("/records")
public R<ShareRecordPageVO> records(@RequestBody ShareRecordQueryDTO query) {
return R.ok(apiShareService.listShareRecords(getUserId(), query.getSubUserId(), query.getPageNum(), query.getPageSize()));
}
}
@@ -0,0 +1,153 @@
package com.ruoyi.user.controller;
import com.github.pagehelper.PageHelper;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.common.utils.DictUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.contract.api.request.ExchangeOrnamentsId;
import com.ruoyi.user.contract.api.request.GetShoppingValueRequest;
import com.ruoyi.user.contract.api.request.IntegratingConversionRequest;
import com.ruoyi.user.service.ApiShoppingService;
import com.ruoyi.domain.other.ApiShoppingBody;
import com.ruoyi.domain.vo.ApiShoppingDataVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Api(tags = "商城")
@Slf4j
@RestController
@RequestMapping("/api/shopping")
public class ApiShoppingController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiShoppingService apiShoppingService;
private final TtUserService userService;
@org.springframework.beans.factory.annotation.Autowired
private RedisLock redisLock;
public ApiShoppingController(ISysConfigService sysConfigService,
ApiShoppingService apiShoppingService,
TtUserService userService) {
this.sysConfigService = sysConfigService;
this.apiShoppingService = apiShoppingService;
this.userService = userService;
}
@ApiOperation("获取不同类型的商品")
@PostMapping("/getShoppingQuery")
public R<List<Map<String, String>>> getShoppingQuery(@RequestBody GetShoppingValueRequest request){
List<SysDictData> dictCache = null;
String value = request.getValue();
if ("0".equals(value)) dictCache = DictUtils.getDictCache("ornaments_type_name");
if ("1".equals(value)) dictCache = DictUtils.getDictCache("ornaments_rarity_name");
if ("2".equals(value)) dictCache = DictUtils.getDictCache("ornaments_exterior_name");
if ("3".equals(value)) dictCache = DictUtils.getDictCache("ornaments_quality_name");
if (StringUtils.isNull(dictCache)) return R.fail("数据异常!!");
List<Map<String, String>> list = dictCache.stream().map(sysDictData -> {
Map<String, String> map = new HashMap<>();
map.put(sysDictData.getDictLabel(), sysDictData.getDictValue());
return map;
}).collect(Collectors.toList());
return R.ok(list);
}
@ApiOperation("商品列表")
@PostMapping("/list")
public PageDataInfo<ApiShoppingDataVO> list(@RequestBody @Validated ApiShoppingBody shoppingBody) {
// 仅显示可兑换:必须拿到当前登录用户的积分才能过滤。
// 未登录且勾选了开关:视为 0 积分,过滤后大概率为空,避免误导用户以为都能兑换。
// 未登录且未勾选:userCredits=null,service 层不会加过滤。
BigDecimal userCredits = null;
if (Boolean.TRUE.equals(shoppingBody.getIsExchange())) {
try {
TtUser ttUser = userService.getById(getUserId());
userCredits = (ttUser != null && ttUser.getAccountCredits() != null)
? ttUser.getAccountCredits()
: BigDecimal.ZERO;
} catch (Exception ignore) {
// 未登录 / token 失效,按 0 积分处理
userCredits = BigDecimal.ZERO;
}
}
PageHelper.startPage(shoppingBody.getPageNum(), shoppingBody.getPageSize());
List<ApiShoppingDataVO> list = apiShoppingService.list(shoppingBody, userCredits);
return getPageData(list);
}
@ApiOperation("商品兑换")
@PostMapping("/exchange")
@RateLimiter(time = 60, count = 30, limitType = LimitType.IP)
@UpdateUserCache
public R exchange(@RequestBody ExchangeOrnamentsId exchangeOrnamentsId) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String shoppingMaintenance = sysConfigService.selectConfigByKey("shoppingMaintenance");
if ("1".equals(shoppingMaintenance)) {
return R.fail("商城功能正在维护中......");
}
TtUser ttUser = userService.getById(getUserId());
// 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务,解锁晚于提交,
// 避免弱网重复提交导致重复扣费/重复发放(锁粒度=单用户,不影响其他用户)
String lockKey = RedisConstants.SHOPPING_EXCHANGE_LOCK + ttUser.getUserId();
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
log.warn("商城兑换加锁失败, userId={}", ttUser.getUserId());
return R.fail("购买进行中,请勿重复操作");
}
try {
// P1#2: 锁内重新读取用户信息,避免 TOCTOU 竞态
ttUser = userService.getById(ttUser.getUserId());
return apiShoppingService.exchange(ttUser, exchangeOrnamentsId.getOrnamentsIds());
} finally {
redisLock.unlock(lockKey);
}
}
@ApiOperation("积分转换")
@UpdateUserCache
@RateLimiter(time = 60, count = 30, limitType = LimitType.IP)
@PostMapping("/integratingConversion")
public R<Boolean> integratingConversion(@RequestBody IntegratingConversionRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......");
TtUser ttUser = userService.getById(getUserId());
// 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务,
// 避免弱网重复提交导致重复转换(锁粒度=单用户,不影响其他用户)
String lockKey = RedisConstants.SHOPPING_INTEGRATING_CONVERSION_LOCK + ttUser.getUserId();
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
return R.fail("积分转换处理中,请勿重复操作");
}
try {
String msg = apiShoppingService.integratingConversion(ttUser, request.getCredits());
return StringUtils.isEmpty(msg) ? R.ok(true, "积分转换成功!") : R.fail(msg);
} finally {
redisLock.unlock(lockKey);
}
}
}
@@ -0,0 +1,55 @@
package com.ruoyi.user.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.user.contract.api.request.GetRewardRequest;
import com.ruoyi.user.model.TtTaskCenterUser;
import com.ruoyi.user.model.vo.ApiTaskCenterVO;
import com.ruoyi.user.service.ApiTaskCenterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
@Api(tags = "任务中心")
@RestController
@RequestMapping("/api/taskCenter")
public class ApiTaskCenterController extends BaseController {
@Autowired
private ApiTaskCenterService apiTaskCenterService;
@ApiOperation("查询任务列表")
@ApiResponse(code = 200, message = "status【领取条件(0不满足 1满足)】" +
"<br>claimed【是否已领取(1已领取)】")
@PreAuthorize("isAuthenticated()")
@PostMapping("/list")
public TableDataInfo list() {
startPage();
Long userId = getUserId();
List<ApiTaskCenterVO> list = apiTaskCenterService.selectApiTaskCenterVOList(userId.intValue());
return getDataTable(list);
}
@ApiOperation("领取任务奖励")
@PreAuthorize("isAuthenticated()")
@PostMapping("/getReward")
public AjaxResult getReward(@RequestBody GetRewardRequest request) {
Long userId = getUserId();
// 判断是否具备领取条件
TtTaskCenterUser ttTaskCenterUser = apiTaskCenterService.selectTtTaskCenterUserByTaskIdAndUserId(request.getTaskId(), userId.intValue());
if (Objects.isNull(ttTaskCenterUser)) {
return AjaxResult.error("不具备领取条件");
}
if ("1".equals(ttTaskCenterUser.getClaimed())) {
return AjaxResult.error("已领取过");
}
return apiTaskCenterService.getReward(request.getTaskId(), userId.intValue());
}
}
@@ -0,0 +1,657 @@
package com.ruoyi.user.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtPromotionUpdateMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtDeliveryRecordService;
import com.ruoyi.admin.service.TtOrderService;
import com.ruoyi.admin.service.TtRechargeRankingRewardService;
import com.ruoyi.admin.service.TtUserAmountRecordsService;
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.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.MoneyUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.dto.sys.TeamUsersParam;
import com.ruoyi.domain.dto.userRecord.AmountRecordsDetailCondition;
import com.ruoyi.domain.dto.userRecord.DeliveryRecordsConfition;
import com.ruoyi.domain.dto.userRecord.OrderCondition;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.other.TtRechargeRankingReward;
import com.ruoyi.domain.vo.TeamDetailVO;
import com.ruoyi.domain.vo.TtUserAccountRecordsRankVO;
import com.ruoyi.domain.vo.TtUserAmountRecords.UserAmountDetailVO;
import com.ruoyi.domain.vo.UserBERankVO;
import com.ruoyi.domain.vo.delivery.DeliveryRecordVO;
import com.ruoyi.domain.vo.order.TtOrderVO;
import com.ruoyi.domain.vo.sys.SimpleTtUserVO;
import com.ruoyi.user.contract.api.request.AmountRankRequest;
import com.ruoyi.user.contract.api.request.PWelfareRecordsRequest;
import com.ruoyi.user.contract.api.request.PersonalRecordRequest;
import com.ruoyi.user.contract.api.response.PersonalRecordResponse;
import com.ruoyi.user.mapper.ApiUserBlendErcashMapper;
import com.ruoyi.user.model.vo.ApiConsumeRankingVO;
import com.ruoyi.user.model.vo.ApiRechargeRankingVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
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.sql.Timestamp;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtAccountRecordSource.RECEIVE_RED_PACKET;
import static com.ruoyi.domain.common.constant.TtAccountRecordSource.RECHARGE;
@Api(tags = "用户充值记录")
@RestController
@RequestMapping("/api/userAmountRecords")
@Slf4j
public class ApiUserAmountRecordsController extends BaseController {
@Autowired
private TtUserAmountRecordsService ttUserAmountRecordsService;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private TtUserAmountRecordsService userAmountRecordsService;
@Autowired
private TtUserService userService;
@Autowired
private TtUserMapper userMapper;
@Autowired
private TtOrderService orderService;
@Autowired
private TtDeliveryRecordService ttDeliveryRecordService;
@Autowired
private TtOrderService ttOrderService;
@Autowired
private TtPromotionUpdateMapper ttPromotionUpdateMapper;
@Autowired
private ApiUserBlendErcashMapper apiUserBlendErcashMapper;
@Autowired
private TtRechargeRankingRewardService ttRechargeRankingRewardService;
/**
* 充值网流水排行榜
* 1今天 2昨天 3近一周
*/
@ApiOperation("充值网流水排行榜")
@Anonymous
@PostMapping("/amountRank")
public Page<TtUserAccountRecordsRankVO> amountRank(@RequestBody AmountRankRequest request) {
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp end = null;
Timestamp begin = null;
if (request.getType().equals(1)) {
begin = new Timestamp(c.getTimeInMillis());
end = new Timestamp(System.currentTimeMillis());
} else if (request.getType().equals(2)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
} else if (request.getType().equals(3)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.WEEK_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
}
return ttUserAmountRecordsService.rank(begin, end, request.getPage(), request.getSize());
}
/**
* 充值网推广奖励明细
*/
@ApiOperation("充值网推广奖励明细")
@PostMapping("/pWelfareRecords")
public R pWelfareRecords(@RequestBody PWelfareRecordsRequest request) {
if (request.getPage() < 1) {
return R.fail("page > 0");
}
if (request.getSize() > 21) {
return R.fail("size <= 20 !!!");
}
Long userId = getUserId();
return ttUserAmountRecordsService.pWelfareRecords(userId.intValue(), request.getPage(), request.getSize());
}
/**
* 综合排行榜
*/
@ApiOperation("流水排行榜")
@PostMapping("/blendErcashRank")
public R<List<UserBERankVO>> blendErcashRank(@RequestBody RankParam param) {
param.setPage(1);
param.setSize(10);
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp end = null;
Timestamp begin = null;
if (param.getType().equals(1)) {
begin = new Timestamp(c.getTimeInMillis());
end = new Timestamp(System.currentTimeMillis());
} else if (param.getType().equals(2)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
} else if (param.getType().equals(3)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.WEEK_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String beginT = dateFormat.format(begin);
String endT = dateFormat.format(end);
if (StringUtils.isBlank(beginT) || StringUtils.isBlank(beginT)) {
beginT = null;
endT = null;
}
List<Integer> source = new ArrayList<>(TtAccountRecordSource.getGameConsumeCodes());
Integer limit = (param.getPage() - 1) * param.getSize();
List<UserBERankVO> rank = ttUserBlendErcashMapper.rank(
source,
beginT,
endT,
limit,
param.getSize());
for (int i = 1; i <= rank.size(); i++) {
var item = rank.get(i - 1);
item.setBeRank(i);
item.setValue(MoneyUtil.toStr(item.getAmount().abs()));
}
return R.ok(rank);
}
@ApiOperation("获取个人综合收支明细")
@PostMapping("/userAccountDetail")
public R<List<UserAmountDetailVO>> userAccountDetail(@RequestBody @Validated AmountRecordsDetailCondition param) {
param.setUserId(null);
param.setUserId(getUserId().intValue());
List<UserAmountDetailVO> list = userAmountRecordsService.userAccountDetail(param);
return R.ok(list);
}
@ApiOperation("获取个人记录")
@PostMapping("/personalRecord")
public R<PersonalRecordResponse> queryPersonalRecord(@RequestBody @Validated PersonalRecordRequest request) {
final int TYPE_TODAY = 1;
final int TYPE_YESTERDAY = 2;
final int TYPE_LAST_7_DAYS = 3;
final int SOURCE_RECHARGE = 1;
final int SOURCE_CONSUME = 2;
final int SOURCE_RED_PACKET = 3;
final int Filtering_Options_OutPut = 0;
final int Filtering_Options_Input = 1;
if (request.getType() != TYPE_TODAY && request.getType() != TYPE_YESTERDAY && request.getType() != TYPE_LAST_7_DAYS) {
return R.fail("参数错误: type 无效");
}
if (request.getSource() != SOURCE_RECHARGE && request.getSource() != SOURCE_CONSUME && request.getSource() != SOURCE_RED_PACKET) {
return R.fail("参数错误: source 无效");
}
Integer uid = getUserId().intValue();
Page<TtUserBlendErcash> page = new Page<>(request.getPage(), request.getPageSize());
// page.setOptimizeCountSql(false);
LambdaQueryWrapper<TtUserBlendErcash> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtUserBlendErcash::getUserId, uid);
// 1:今日 2:昨日 3:最近7天
LocalDate today = LocalDate.now();
switch (request.getType()) {
case TYPE_TODAY:
LocalDateTime startOfToday = today.atStartOfDay();
LocalDateTime endOfToday = today.plusDays(1).atStartOfDay().minusNanos(1);
wrapper.between(TtUserBlendErcash::getCreateTime, startOfToday, endOfToday);
break;
case TYPE_YESTERDAY:
LocalDateTime startOfYesterday = today.minusDays(1).atStartOfDay();
LocalDateTime endOfYesterday = today.atStartOfDay().minusNanos(1);
wrapper.between(TtUserBlendErcash::getCreateTime, startOfYesterday, endOfYesterday);
break;
case TYPE_LAST_7_DAYS:
LocalDateTime startOf7Days = today.minusDays(6).atStartOfDay();
wrapper.ge(TtUserBlendErcash::getCreateTime, startOf7Days);
break;
default:
// 理论上不会进入,因为上面已经校验过
break;
}
// 查询的数据源 1:充值 2:全部/支出/收入 3:红包
switch (request.getSource()) {
case SOURCE_RECHARGE:
wrapper.eq(TtUserBlendErcash::getSource, RECHARGE.getCode());
break;
case SOURCE_CONSUME:
//wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.OUTPUT);
if (request.getFilteringOptions() != null) {
if (request.getFilteringOptions() == Filtering_Options_OutPut) {
wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.OUTPUT.getCode());
} else if (request.getFilteringOptions() == Filtering_Options_Input) {
wrapper.eq(TtUserBlendErcash::getType, TtAccountRecordType.INPUT.getCode());
}
}
break;
case SOURCE_RED_PACKET:
wrapper.eq(TtUserBlendErcash::getSource, RECEIVE_RED_PACKET.getCode());
break;
default:
break;
}
wrapper.orderByDesc(TtUserBlendErcash::getCreateTime);
IPage<TtUserBlendErcash> listPage = ttUserBlendErcashMapper.selectPage(page, wrapper);
List<TtUserBlendErcash> userList = listPage.getRecords();
var formatter = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss");
var r = userList.stream().map(v -> {
PersonalRecordResponse.RecordVO item = new PersonalRecordResponse.RecordVO();
item.setAmount(MoneyUtil.toStr(v.getAmount()));
item.setFinalAmount(MoneyUtil.toStr(v.getFinalAmount()));
item.setRemark(v.getRemark());
item.setDatetime(v.getCreateTime().toLocalDateTime().format(formatter));
item.setSource(v.getSource());
return item;
}).collect(Collectors.toList());
PersonalRecordResponse response = new PersonalRecordResponse();
response.setRows(r);
response.setTotal(listPage.getTotal());
return R.ok(response);
}
@ApiOperation("获取个人提货记录")
@PostMapping("/deliveryRecords")
public R deliveryRecords(@RequestBody @Validated DeliveryRecordsConfition param) {
param.setUIdList(null);
int userId = getUserId().intValue();
if (ObjectUtil.isNull(userId)) return R.fail(401, "登录过期请重新登录。");
param.setUIdList(Arrays.asList(userId));
List<DeliveryRecordVO> list = ttDeliveryRecordService.byCondition(param);
return R.ok(list);
}
@ApiOperation("获取团队用户列表")
// @Anonymous
@PostMapping("/teamUsers")
public R<List<SimpleTtUserVO>> teamUsers(@RequestBody @Validated TeamUsersParam param) {
param.setBossIds(null);
param.setEmployeeIds(null);
param.setBossIds(Arrays.asList(getUserId().intValue()));
if (ObjectUtil.isNull(param.getOrderByFie())) param.setOrderByFie(1);
// 查询所有下级id
List<Integer> allEmployeesId = userMapper.allEmployeesByParents(param.getBossIds());
if (allEmployeesId.isEmpty()) {
return R.ok(new ArrayList<>());
}
// 解析时间
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date beginTime = null;
try {
beginTime = dateFormat.parse(param.getBeginTime());
} catch (ParseException e) {
return R.fail("日期解析异常,检查日期格式是否正确。");
}
// main
if (param.getOrderByFie().equals(1)) {
// 查询下级的最近一个绑定时间
List<TeamDetailVO> mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId);
if (mit.size() < allEmployeesId.size()) log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!");
// 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准
List<Integer> empIds1 = new ArrayList<>();
List<TeamDetailVO> empIds2 = new ArrayList<>();
for (TeamDetailVO item : mit) {
Timestamp latelyTime = item.getBeginTime();
if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) > 0) {
empIds2.add(item);
} else {
empIds1.add(item.getEmployeeId());
}
}
List<SimpleTtUserVO> batchRechargeTotal1 = new ArrayList<>();
if (!empIds1.isEmpty()) {
// 主表充值统计
batchRechargeTotal1 = orderService.batchRechargeTotal(
empIds1,
param.getBeginTime(),
param.getEndTime(),
param.getOrderType(),
param.getPage(),
param.getSize());
System.err.println(batchRechargeTotal1);
}
List<SimpleTtUserVO> batchRechargeTotal2 = new ArrayList<>();
if (!empIds2.isEmpty()) {
for (TeamDetailVO vo : empIds2) {
String beginT = dateFormat.format(vo.getBeginTime());
List<SimpleTtUserVO> data = orderService.batchRechargeTotal(
Arrays.asList(vo.getEmployeeId()),
beginT,
param.getEndTime(),
param.getOrderType(),
param.getPage(),
param.getSize());
batchRechargeTotal2.addAll(data);
}
}
batchRechargeTotal1.addAll(batchRechargeTotal2);
return R.ok(batchRechargeTotal1);
} else if (param.getOrderByFie().equals(2) || param.getOrderByFie().equals(3) || param.getOrderByFie().equals(4)) {
if (StringUtils.isBlank(param.getBeginTime())) {
param.setBeginTime(null);
}
if (StringUtils.isBlank(param.getEndTime())) {
param.setEndTime(null);
}
if (ObjectUtil.isNull(param.getOrderType())) {
param.setOrderType(1);
}
// 查询下级的最近一个绑定时间
List<TeamDetailVO> mit = ttPromotionUpdateMapper.latelyUpdate(allEmployeesId);
if (mit.size() < allEmployeesId.size()) {
log.warn("下级已绑定上级,但未写入更新日志,请及时检查!!!");
}
// 如果最近绑定时间大于本次查询的起始时间,以最近绑定时间为准
List<Integer> empIds1 = new ArrayList<>();
List<TeamDetailVO> empIds2 = new ArrayList<>();
for (TeamDetailVO item : mit) {
Timestamp latelyTime = item.getBeginTime();
if (latelyTime.compareTo(new Timestamp(beginTime.getTime())) > 0) {
empIds2.add(item);
} else {
empIds1.add(item.getEmployeeId());
}
}
List<SimpleTtUserVO> batchConsumeTotal1 = new ArrayList<>();
if (!empIds1.isEmpty()) {
// 主表消费统计
batchConsumeTotal1 = ttUserBlendErcashMapper.batchConsumeTotal(
empIds1,
param.getBeginTime(),
param.getEndTime(),
param.getOrderByFie(),
param.getOrderType(),
(param.getPage() - 1) * param.getSize(),
param.getSize());
}
List<SimpleTtUserVO> batchConsumeTotal2 = new ArrayList<>();
if (!empIds2.isEmpty()) {
for (TeamDetailVO vo : empIds2) {
String beginT = dateFormat.format(vo.getBeginTime());
List<SimpleTtUserVO> data = ttUserBlendErcashMapper.batchConsumeTotal(
Arrays.asList(vo.getEmployeeId()),
beginT,
param.getEndTime(),
param.getOrderByFie(),
param.getOrderType(),
(param.getPage() - 1) * param.getSize(),
param.getSize()
);
batchConsumeTotal2.addAll(data);
}
}
batchConsumeTotal1.addAll(batchConsumeTotal2);
return R.ok(batchConsumeTotal1);
} else {
return R.fail("非法的排序字段");
}
// return userService.teamUsers(param);
}
@ApiOperation("获取个人充值明细")
@PostMapping("/rechargeRecords")
public R rechargeRecords(@RequestBody @Validated OrderCondition param) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) return R.fail(401, "登录过期,请重新登录。");
param.setUserIdList(Arrays.asList(userId.intValue()));
List<TtOrderVO> list = ttOrderService.byCondition(param);
return R.ok(list);
}
@Deprecated
@ApiOperation("获取消费排行榜")
@Anonymous
@PostMapping("/rechargeRanking")
public AjaxResult getRechargeRanking() {
// 创建日期格式化对象,指定日期格式
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
// 获取今天日期
Date today = new Date();
String formattedTodayDate = formatter.format(today);
List<ApiRechargeRankingVO> todayFightRanking = apiUserBlendErcashMapper.getRechargeRankingByDate(formattedTodayDate);
// 获取昨天日期
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
Date yesterday = cal.getTime();
String formattedYesterdayDate = formatter.format(yesterday);
List<ApiRechargeRankingVO> yesterdayFightRanking = apiUserBlendErcashMapper.getRechargeRankingByDate(formattedYesterdayDate);
Map<String, List<ApiRechargeRankingVO>> map = new HashMap<>();
map.put("todayFightRanking", todayFightRanking);
map.put("yesterdayFightRanking", yesterdayFightRanking);
return AjaxResult.success(map);
}
@ApiOperation("消费排行榜奖励说明")
@PostMapping("/rechargeRankingRewardsIntroduction")
public AjaxResult rechargeRankingRewardsIntroduction() {
List<TtRechargeRankingReward> ttRechargeRankingRewards = ttRechargeRankingRewardService.selectTtRechargeRankingRewardList(null);
ttRechargeRankingRewards.forEach(r -> r.setRewardStr(MoneyUtil.toStr(r.getReward())));
return success(ttRechargeRankingRewards);
}
/**
* 今日消费流水排行榜 Top10。
* 统计口径:tt_user_blend_ercash 中 type=0(OUTPUT)且 source 命中
*排除机器人 (user_type='03'),按总消费降序,返回前 10。
*/
@ApiOperation("今日消费流水排行榜 Top10")
@Anonymous
@PostMapping("/consumeRanking/top10")
public R<List<ApiConsumeRankingVO>> consumeRankingTop10() {
// 今日起止时间
LocalDate today = LocalDate.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String beginT = today.atStartOfDay().format(fmt);
String endT = today.plusDays(1).atStartOfDay().minusNanos(1).format(fmt);
List<Integer> sourceList = new ArrayList<>(TtAccountRecordSource.getGameBetCodes());
List<ApiConsumeRankingVO> list = apiUserBlendErcashMapper.topConsumeRanking(beginT, endT, sourceList, 10);
if (list == null) list = new ArrayList<>();
// 填充格式化金额 与 各 TOP 的奖励金额
for (ApiConsumeRankingVO vo : list) {
vo.setTotalConsumeStr(MoneyUtil.toStr(vo.getTotalConsume()));
BigDecimal reward = BigDecimal.ZERO;
try {
int rankInt = Integer.parseInt(vo.getRank());
TtRechargeRankingReward cfg = ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(rankInt);
if (cfg != null && cfg.getReward() != null) {
reward = cfg.getReward();
}
} catch (NumberFormatException ignore) {
// rank 不是数字(理论上 Top10 不会出现),奖励保持 0
}
vo.setReward(reward);
vo.setRewardStr(MoneyUtil.toStr(reward));
}
return R.ok(list);
}
/**
* 今日消费流水排行榜中【当前登录用户】的个人排名。
*/
@ApiOperation("今日消费流水个人排名")
@PostMapping("/consumeRanking/myRank")
public R<ApiConsumeRankingVO> consumeRankingMyRank() {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail(401, "登录过期,请重新登录。");
}
com.ruoyi.domain.entity.sys.TtUser user = userService.getById(userId);
LocalDate today = LocalDate.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String beginT = today.atStartOfDay().format(fmt);
String endT = today.plusDays(1).atStartOfDay().minusNanos(1).format(fmt);
List<Integer> sourceList = new ArrayList<>(TtAccountRecordSource.getGameBetCodes());
// 个人今日总消费
BigDecimal myTotal = apiUserBlendErcashMapper.userConsumeTotal(userId, beginT, endT, sourceList);
if (myTotal == null) myTotal = BigDecimal.ZERO;
ApiConsumeRankingVO vo = new ApiConsumeRankingVO();
vo.setUserId(userId);
if (user != null) {
vo.setNickName(user.getNickName());
vo.setAvatar(user.getAvatar());
}
vo.setTotalConsume(myTotal);
vo.setTotalConsumeStr(MoneyUtil.toStr(myTotal));
// 今日无消费直接不上榜
if (myTotal.compareTo(BigDecimal.ZERO) <= 0) {
vo.setRank("---");
vo.setReward(BigDecimal.ZERO);
vo.setRewardStr(MoneyUtil.toStr(BigDecimal.ZERO));
return R.ok(vo);
}
// 排名 = 消费严格大于我的人数 + 1
Integer ahead = apiUserBlendErcashMapper.countUsersAheadOf(myTotal, beginT, endT, sourceList);
int rank = (ahead == null ? 0 : ahead) + 1;
if (rank > 10) {
vo.setRank("---");
vo.setReward(BigDecimal.ZERO);
vo.setRewardStr(MoneyUtil.toStr(BigDecimal.ZERO));
} else {
vo.setRank(String.valueOf(rank));
TtRechargeRankingReward cfg = ttRechargeRankingRewardService.selectTtRechargeRankingRewardById(rank);
BigDecimal reward = (cfg != null && cfg.getReward() != null) ? cfg.getReward() : BigDecimal.ZERO;
vo.setReward(reward);
vo.setRewardStr(MoneyUtil.toStr(reward));
}
return R.ok(vo);
}
public R checkLogin() {
Long userId;
try {
userId = getUserId();
if (ObjectUtil.isEmpty(userId)) AjaxResult.error(401, "登录过期,请重新登录。");
return R.ok(userId);
} catch (Exception e) {
return R.fail(401, "登录过期,请重新登录。");
}
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class RankParam {
@ApiModelProperty("类型(1今天 2昨天 3近七天)")
@NotEmpty(message = "时间区间不能为空")
private Integer type;
@Min(value = 1, message = "最小1")
private Integer page;
@Min(value = 1, message = "最小1")
private Integer size;
}
}
@@ -0,0 +1,548 @@
package com.ruoyi.user.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.mapper.TtOrderMapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.service.TtDeliveryRecordService;
import com.ruoyi.admin.service.TtUserAmountRecordsService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.service.TtVipLevelService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.constant.CacheConstants;
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.page.PageDataInfo;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrder;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.delivery.TtDeliveryRecord;
import com.ruoyi.domain.entity.recorde.TtUserAmountRecords;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiForgetPasswordBody;
import com.ruoyi.domain.other.ApiSafePasswordBody;
import com.ruoyi.domain.other.ApiUpdateUserDetailsBody;
import com.ruoyi.domain.other.RealNameAuthenticationBody;
import com.ruoyi.domain.other.TtDeliveryRecordBody;
import com.ruoyi.domain.other.TtUserAmountRecordsBody;
import com.ruoyi.domain.other.TtVipLevel;
import com.ruoyi.domain.vo.TtDeliveryRecordDataVO;
import com.ruoyi.domain.vo.TtUserPackSackDataVO;
import com.ruoyi.domain.vo.TtUserVipLevelVo;
import com.ruoyi.domain.vo.TtUserVipProgressVo;
import com.ruoyi.domain.vo.VipLevelCountVo;
import com.ruoyi.domain.vo.user.TtUseChildInfoVo;
import com.ruoyi.domain.vo.user.TtUserInfoVo;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.baidu.service.BaiduService;
import com.ruoyi.user.contract.api.request.*;
import com.ruoyi.user.contract.api.response.PeakMomentResponse;
import com.ruoyi.user.service.ApiUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.*;
import java.util.stream.Collectors;
// 解析 fightBisectPrize 配置(饰品ID数组 JSON)
import cn.hutool.json.JSONUtil;
@Api(tags = "用户管理")
@RestController
@RequestMapping("/api/user")
@Slf4j
public class ApiUserController extends BaseController {
private final ISysConfigService sysConfigService;
private final TtUserService userService;
private final ApiUserService apiUserService;
private final TtOrderMapper ttOrderMapper;
private final TtBoxRecordsMapper ttBoxRecordsMapper;
private final TtOrnamentMapper ttOrnamentMapper;
private final TtDeliveryRecordService ttDeliveryRecordService;
private final TtUserAmountRecordsService userAmountRecordsService;
private final TtDeliveryRecordService deliveryRecordService;
public ApiUserController(ISysConfigService sysConfigService,
TtUserService userService,
TtDeliveryRecordService ttDeliveryRecordService,
ApiUserService apiUserService, TtOrderMapper ttOrderMapper,
TtBoxRecordsMapper ttBoxRecordsMapper,
TtOrnamentMapper ttOrnamentMapper,
TtUserAmountRecordsService userAmountRecordsService,
TtDeliveryRecordService deliveryRecordService) {
this.sysConfigService = sysConfigService;
this.userService = userService;
this.ttDeliveryRecordService = ttDeliveryRecordService;
this.apiUserService = apiUserService;
this.ttOrderMapper = ttOrderMapper;
this.ttBoxRecordsMapper = ttBoxRecordsMapper;
this.ttOrnamentMapper = ttOrnamentMapper;
this.userAmountRecordsService = userAmountRecordsService;
this.deliveryRecordService = deliveryRecordService;
}
@Autowired
private TtVipLevelService vipLevelService;
@Autowired
private BaiduService baiduService;
@ApiOperation("头像上传")
@UpdateUserCache
@PostMapping(value = "/profilePictureUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Object> profilePictureUpload(@RequestPart("file") MultipartFile file) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
if (StringUtils.isNull(file)) {
return R.fail("未选择头像文件!");
}
TtUser ttUser = userService.getById(getUserId());
String msg = apiUserService.profilePictureUpload(ttUser, file);
return StringUtils.isEmpty(msg) ? R.ok("头像上传成功!") : R.fail(msg);
}
@ApiOperation("更新个人信息")
@UpdateUserCache
@PostMapping("/updateUserDetails")
public R<Object> updateUserDetails(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
String msg = apiUserService.updateUserDetails(ttUser, updateUserDetailsBody);
return StringUtils.isEmpty(msg) ? R.ok("个人信息更新成功!") : R.fail(msg);
}
@ApiOperation("更新收货地址")
@UpdateUserCache
@PostMapping("/updateDeliveryAddress")
public R<Object> updateDeliveryAddress(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
String msg = apiUserService.updateUserDetails(ttUser, updateUserDetailsBody);
return StringUtils.isEmpty(msg) ? R.ok("收货地址更新成功!") : R.fail(msg);
}
@ApiOperation("绑定推广人")
@UpdateUserCache
@PostMapping("/bindBoss")
public R bindBoss(@RequestBody ApiUpdateUserDetailsBody updateUserDetailsBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiUserService.bindBoss(ttUser, updateUserDetailsBody);
}
@ApiOperation("绑定邀请码")
@UpdateUserCache
@PostMapping("/bindInvitationCode")
public R bindInvitationCode(@RequestBody @Validated BindInvitationCodeRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiUserService.bindInvitationCode(ttUser, request.getInvitationCode());
}
@ApiOperation("修改密码")
// @UpdateUserCache
@PostMapping("/changePW")
public R<Object> changePW(@RequestBody @Validated ApiUpdateUserDetailsBody updateUserDetailsBody, HttpServletRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
String token = CacheConstants.LOGIN_TOKEN_KEY + getToken();
return apiUserService.changePW(ttUser, updateUserDetailsBody, token);
}
@ApiOperation("忘记密码")
@PostMapping("/forgetPassword")
public R<Boolean> forgetPassword(@RequestBody ApiForgetPasswordBody apiForgetPasswordBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String msg = apiUserService.forgetPassword(apiForgetPasswordBody);
return StringUtils.isEmpty(msg) ? R.ok(true, "密码修改成功!") : R.fail(msg);
}
@ApiOperation("设置安全密码(提货校验用,与登录密码独立)")
@PostMapping("/setSafePassword")
public R<Object> setSafePassword(@RequestBody ApiSafePasswordBody apiSafePasswordBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return apiUserService.setSafePassword(ttUser, apiSafePasswordBody);
}
@ApiOperation("实名认证")
@RepeatSubmit(interval = 60000, message = "操作过于频繁,请60秒后重试!")
@UpdateUserCache
@PostMapping("/realNameAuthentication")
public R<String> realNameAuthentication(@RequestBody RealNameAuthenticationBody realNameAuthenticationBody) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtUser ttUser = userService.getById(getUserId());
String msg = apiUserService.realNameAuthentication2(ttUser, realNameAuthenticationBody.getRealName(),
realNameAuthenticationBody.getIdNum(), realNameAuthenticationBody.getPhoneNumber(),
realNameAuthenticationBody.getCaptcha());
return msg.startsWith("alipays") ? R.ok(msg) : R.fail(msg);
}
@ApiOperation("认证成功")
@UpdateUserCache
@PostMapping("/authenticationOk")
public R<Boolean> authenticationOk() {
TtUser ttUser = userService.getById(getUserId());
String msg = apiUserService.authenticationOk(ttUser);
return StringUtils.isEmpty(msg) ? R.ok(true, "认证成功!") : R.fail(false, msg);
}
@ApiOperation("获取下级人数")
// @UpdateUserCache
@PostMapping("/rechargeCount")
public R<Integer> rechargeCount() {
TtUser ttUser = userService.getById(getUserId());
return R.ok((int) userService.count(new QueryWrapper<TtUser>().eq("parent_id", ttUser.getUserId())));
}
@ApiOperation("获取下级提取数")
@UpdateUserCache
@PostMapping("/getExtracts")
public R<BigDecimal> getExtracts() {
TtUser ttUser = userService.getById(getUserId());
List<TtUser> ttUsers = userService.list(new QueryWrapper<TtUser>().eq("parent_id", ttUser.getUserId()));
// 判断是否有下级用户
if (ttUsers == null || ttUsers.size() == 0) {
return R.ok(new BigDecimal(0));
}
List<Integer> collect = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList());
List<TtDeliveryRecord> ttDeliveryRecords = ttDeliveryRecordService.list(new QueryWrapper<TtDeliveryRecord>().in("user_id", collect).eq("status", "10"));
BigDecimal bigDecimal = new BigDecimal(0);
ttDeliveryRecords.forEach(o -> {
bigDecimal.add(o.getOrnamentsPrice());
});
return R.ok(bigDecimal);
}
@ApiOperation("获取下级充值总金额")
// @UpdateUserCache
@PostMapping("/getOrdersAmounts")
public R<BigDecimal> getOrdersAmounts() {
TtUser ttUser = userService.getById(getUserId());
List<TtUser> ttUsers = userService.list(new QueryWrapper<TtUser>().eq("parent_id", ttUser.getUserId()));
// 判断是否有下级用户
if (ttUsers == null || ttUsers.size() == 0) {
return R.ok(new BigDecimal(0));
}
List<Integer> collect = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList());
List<TtOrder> ttOrders = ttOrderMapper.selectList(new QueryWrapper<TtOrder>().in("user_id", collect).and(k -> k.eq("status", "3").or().eq("status", "4")));
BigDecimal bigDecimal = new BigDecimal(0);
ttOrders.forEach(o -> {
bigDecimal.add(o.getGoodsPrice());
});
return R.ok(bigDecimal);
}
@ApiOperation("获取下级流水详情")
@PostMapping("/getLsjlList")
public PageDataInfo<TtUserAmountRecords> getLsjlList(@RequestBody TtUserAmountRecordsBody ttUserAmountRecordsBody) {
startPage();
TtUser ttUser = userService.getById(getUserId());
List<TtUser> ttUsers = userService.list(new QueryWrapper<TtUser>().eq("parent_id", ttUser.getUserId()));
// 判断是否有下级用户
if (ttUsers == null || ttUsers.size() == 0) {
return getPageData(new ArrayList<TtUserAmountRecords>());
}
List<Integer> userIds = ttUsers.stream().map(i -> i.getUserId()).collect(Collectors.toList());
QueryWrapper<TtUserAmountRecords> ttUserAmountRecordsQueryWrapper = new QueryWrapper<>();
// 时间段筛选
if (ttUserAmountRecordsBody.getStartTime() != null) {
ttUserAmountRecordsQueryWrapper.ge("create_time", ttUserAmountRecordsBody.getStartTime());
}
if (ttUserAmountRecordsBody.getEndTime() != null) {
ttUserAmountRecordsQueryWrapper.lt("create_time", ttUserAmountRecordsBody.getEndTime());
}
if (ttUserAmountRecordsBody.getUserId() != null) {
ttUserAmountRecordsQueryWrapper.eq("user_id", ttUserAmountRecordsBody.getUserId());
}
List<TtUserAmountRecords> list = userAmountRecordsService.list(ttUserAmountRecordsQueryWrapper.in("user_id", userIds).orderByDesc("create_time"));
return getPageData(list);
}
@ApiOperation("获取出货记录")
@PostMapping("/getDeliveryRecordList")
public PageDataInfo<TtDeliveryRecordDataVO> getDeliveryRecordList(@RequestBody TtDeliveryRecordBody deliveryRecordBody) {
startPage();
TtUser ttUser = userService.getById(getUserId());
deliveryRecordBody.setUserId(ttUser.getUserId());
List<TtDeliveryRecordDataVO> list = deliveryRecordService.getDeliveryRecordByUserList(deliveryRecordBody);
return getPageData(list);
}
/**
* 每日出货排行榜1今天 2昨天 3近一周
*/
@ApiOperation("每日出货排行榜")
@PostMapping("/propRankOfDa")
public R propRankOfDay(@RequestBody PropRankOfDayRequest request) {
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY, 0);
c.set(Calendar.MINUTE, 0);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
Timestamp end = null;
Timestamp begin = null;
if (request.getType().equals(1)) {
begin = new Timestamp(c.getTimeInMillis());
end = new Timestamp(System.currentTimeMillis());
} else if (request.getType().equals(2)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
} else if (request.getType().equals(3)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.WEEK_OF_MONTH, -1);
begin = new Timestamp(c.getTimeInMillis());
}
request.setNumber(10);
List<TtUserPackSackDataVO> list = userService.propRankOfDay(begin, end, request.getNumber());
// return getPageData(list);
return R.ok(list);
}
@ApiOperation("VIP等级说明")
@PostMapping("/vipLevelIntroduction")
public AjaxResult vipLevelIntroduction() {
List<TtVipLevel> list = vipLevelService.list();
return success(list);
}
@ApiOperation("VIP等级转赠/提货次数配置")
@PostMapping("/vipLevelCounts")
public AjaxResult vipLevelCounts() {
List<TtVipLevel> list = vipLevelService.list();
List<VipLevelCountVo> result = list.stream()
.sorted(Comparator.comparing(TtVipLevel::getId,
Comparator.nullsLast(Comparator.naturalOrder())))
.map(v -> {
VipLevelCountVo vo = new VipLevelCountVo();
vo.setId(v.getId());
vo.setName(v.getName());
vo.setTransferCount(v.getTransferCount() != null ? v.getTransferCount() : 0);
vo.setDeliveryCount(v.getDeliveryCount() != null ? v.getDeliveryCount() : 0);
return vo;
})
.collect(Collectors.toList());
return success(result);
}
@ApiOperation("VIP等级进度")
@PostMapping("/userVipLeverRate")
public AjaxResult userVipLeverRate() {
TtUserVipLevelVo vipLevel = userService.userVipLeverRate(getUserId().intValue());
return success(vipLevel);
}
@ApiOperation("VIP等级经验进度(1元=1经验)")
@PostMapping("/vipProgress")
public AjaxResult vipProgress() {
TtUserVipProgressVo vo = userService.userVipProgress(getUserId().intValue());
return success(vo);
}
@ApiOperation("用户信息包含下级信息")
@PostMapping("/userInfo")
public AjaxResult userInfo() {
TtUserInfoVo userInfo = userService.userInfo(getUserId().intValue());
return success(userInfo);
}
@ApiOperation("用户信息包含下级用户信息")
@PostMapping("/userChildInfo")
public AjaxResult userChildInfo() {
List<TtUseChildInfoVo> userInfo = userService.userChildInfos(getUserId().intValue());
return success(userInfo);
}
@ApiOperation("用户领取VIP等级(支持一次传多个等级一起领取)")
@UpdateUserCache
@PostMapping("/userVipLevelRechargeThreshold")
public AjaxResult userVipLevelRechargeThreshold(@RequestBody UserVipLevelRequest request) {
return userService.userVipLevelRechargeThreshold(getUserId().intValue(), request.getVipLevels());
}
@ApiOperation("用户已经领取VIP等级")
@PostMapping("/userHasVipLevelRechargeThreshold")
public AjaxResult userHasVipLevelRechargeThreshold() {
return userService.userHasVipLevelRechargeThreshold(getUserId().intValue());
}
@ApiOperation("检查是否有可领取的VIP等级红包")
@PostMapping("/checkVipLevelClaimable")
public AjaxResult checkVipLevelClaimable() {
return userService.checkVipLevelClaimable(getUserId().intValue());
}
@ApiOperation("用户领取充值门槛达标奖励")
@UpdateUserCache
@PostMapping("/userRechargeBonus")
public AjaxResult userRechargeBonus(@RequestBody UserRechargeBonusRequest request) {
return userService.userRechargeBonus(getUserId().intValue(), request.getRecharge(), request.getBonus());
}
@ApiOperation("用户已领取充值门槛达标奖励")
@PostMapping("/userHasRechargeBonus")
public AjaxResult userHasRechargeBonus() {
return userService.userHasRechargeBonus(getUserId().intValue());
}
@ApiOperation("用户积分兑换金币")
@UpdateUserCache
@PostMapping("/userCreditsToAmount")
public AjaxResult userCreditsToAmount(@RequestBody UserCreditsToAmountRequest request) {
return userService.userCreditsToAmount(getUserId().intValue(), request.getCredits());
}
@ApiOperation("回调百度接口")
@UpdateUserCache
@PostMapping("/baidu")
public AjaxResult baidu(@RequestBody BaiduCallbackRequest request) {
baiduService.upload(getUserId().intValue(), request.getNewType(), request.getBdVid());
return AjaxResult.success();
}
@ApiOperation("巅峰时刻 - 高品质TOP10")
@PostMapping("/peakMoment")
public R<PeakMomentResponse> peakMoment() {
// 获取当前登录用户信息
TtUser ttUser = userService.getById(getUserId());
// 读取对战平分奖配置并排除:平分奖不属于"最高获胜饰品",不应上榜
// fightBisectPrize 存的是饰品ID数组 JSON,如 [32131234354]
List<Long> bisectPrizeIds = parseBisectPrizeIds();
// 查询该用户获得的所有饰品,按价格降序取TOP10(排除对战平分奖饰品)
var chain = new LambdaQueryChainWrapper<>(ttBoxRecordsMapper)
.eq(TtBoxRecords::getHolderUserId, ttUser.getUserId());
if (!bisectPrizeIds.isEmpty()) {
chain.notIn(TtBoxRecords::getOrnamentId, bisectPrizeIds);
}
List<TtBoxRecords> records = chain
.orderByDesc(TtBoxRecords::getOrnamentsPrice)
.last("LIMIT 10")
.list();
// 批量查询饰品信息,获取 exteriorName(外观名称)与 name(完整名称)
Map<Long, TtOrnament> ornamentMap = Map.of();
if (!records.isEmpty()) {
List<Long> ornamentIds = records.stream()
.map(TtBoxRecords::getOrnamentId)
.filter(id -> id != null)
.distinct()
.collect(Collectors.toList());
if (!ornamentIds.isEmpty()) {
List<TtOrnament> ornaments = ttOrnamentMapper.selectByOrnamentIds(ornamentIds);
ornamentMap = ornaments.stream()
.collect(Collectors.toMap(TtOrnament::getId, o -> o, (a, b) -> a));
}
}
// 组装响应
PeakMomentResponse response = new PeakMomentResponse();
response.setUserId(ttUser.getUserId());
response.setNickName(ttUser.getNickName());
response.setAvatar(ttUser.getAvatar());
Map<Long, TtOrnament> finalOrnamentMap = ornamentMap;
List<PeakMomentResponse.PeakItem> items = records.stream().map(r -> {
TtOrnament ornament = r.getOrnamentId() != null ? finalOrnamentMap.get(r.getOrnamentId()) : null;
PeakMomentResponse.PeakItem item = new PeakMomentResponse.PeakItem();
item.setId(r.getId());
item.setOrnamentName(r.getOrnamentName());
// 完整名称优先取 tt_ornament.name,缺失时回退背包记录里的名称
item.setName(ornament != null && ornament.getName() != null && !ornament.getName().isEmpty()
? ornament.getName() : r.getOrnamentName());
item.setImageUrl(r.getImageUrl());
item.setOrnamentLevelImg(r.getOrnamentLevelImg());
item.setExteriorName(ornament != null && ornament.getExteriorName() != null
? ornament.getExteriorName() : "");
item.setOrnamentsPrice(r.getOrnamentsPrice());
item.setSource(r.getSource());
item.setCreateTime(r.getCreateTime());
return item;
}).collect(Collectors.toList());
response.setItems(items);
return R.ok(response);
}
/**
* 解析对战平分奖配置(fightBisectPrize)返回饰品ID列表;
* 未配置 / 无饰品 / 解析失败时返回空列表(不排除任何记录)。
*/
private List<Long> parseBisectPrizeIds() {
try {
String cfg = sysConfigService.selectConfigByKey("fightBisectPrize");
if (StringUtils.isBlank(cfg)) {
return Collections.emptyList();
}
List<Long> ids = JSONUtil.parseArray(cfg).toList(Long.class);
return ids != null ? ids : Collections.emptyList();
} catch (Exception e) {
log.warn("解析 fightBisectPrize 配置失败, 巅峰时刻不排除平分奖", e);
return Collections.emptyList();
}
}
}
@@ -0,0 +1,72 @@
package com.ruoyi.user.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.service.TtUserCreditsRecordsService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.domain.entity.recorde.TtUserCreditsRecords;
import com.ruoyi.domain.vo.TtUserCreditsRecordsRankVO;
import com.ruoyi.user.contract.api.request.CreditsRankRequest;
import com.ruoyi.user.contract.api.request.PWelfareCreditsRecordsRequest;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.sql.Timestamp;
import java.util.Calendar;
@Api(tags = "用户信用记录")
@RestController
@RequestMapping("/api/userCreditsRecords")
@Slf4j
public class ApiUserCreditsRecordsController extends BaseController {
private final TtUserCreditsRecordsService ttUserCreditsRecordsService;
public ApiUserCreditsRecordsController(TtUserCreditsRecordsService ttUserCreditsRecordsService) {
this.ttUserCreditsRecordsService = ttUserCreditsRecordsService;
}
/**
* 积分排行榜 1今天 2昨天 3近一周
*/
@ApiOperation("流水网流水排行榜")
@PostMapping("/creditsRank")
@Anonymous
public Page<TtUserCreditsRecordsRankVO> creditsRank(@RequestBody CreditsRankRequest request) {
Calendar c = Calendar.getInstance();
c.set(Calendar.HOUR_OF_DAY,0);
c.set(Calendar.MINUTE,0);
c.set(Calendar.SECOND,0);
c.set(Calendar.MILLISECOND,0);
Timestamp end = null;
Timestamp begin = null;
if (request.getType().equals(1)) {
begin = new Timestamp(c.getTimeInMillis());
end = new Timestamp(System.currentTimeMillis());
} else if (request.getType().equals(2)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.DAY_OF_MONTH,-1);
begin = new Timestamp(c.getTimeInMillis());
} else if (request.getType().equals(3)) {
end = new Timestamp(c.getTimeInMillis());
c.add(Calendar.WEEK_OF_MONTH,-1);
begin = new Timestamp(c.getTimeInMillis());
}
return ttUserCreditsRecordsService.rank(begin, end, request.getPage(), request.getSize());
}
/**
* 流水网推广奖励明细 1、今天;2、昨天;3、近七天
*/
@ApiOperation("流水网推广奖励明细")
@PostMapping("/pWelfareRecords")
public Page<TtUserCreditsRecords> pWelfareRecords(@RequestBody PWelfareCreditsRecordsRequest request) {
Long userId = getUserId();
return ttUserCreditsRecordsService.pWelfareRecords(Integer.valueOf(String.valueOf(userId)),request.getType(),request.getPage(),request.getSize());
}
}
@@ -0,0 +1,343 @@
package com.ruoyi.user.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import org.springframework.validation.annotation.Validated;
import com.ruoyi.domain.dto.packSack.DecomposeLogCondition;
import com.ruoyi.domain.dto.packSack.DecomposeParam;
import com.ruoyi.domain.dto.packSack.DeliveryParam;
import com.ruoyi.domain.dto.packSack.PackSackCondition;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.BoxTransferRecord;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtOrnamentsLevel;
import com.ruoyi.domain.vo.TtBoxRecordsDataVO;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import com.ruoyi.domain.vo.client.PackSackGlobalData;
import com.ruoyi.domain.vo.client.WelfareBoxVO;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.contract.api.request.QueryTransferRecordRequest;
import com.ruoyi.user.contract.api.response.QueryTransferRecordResponse;
import com.ruoyi.user.mapper.ApiBoxTransferRecordMapper;
import com.ruoyi.user.model.TransferParam;
import com.ruoyi.user.model.vo.ApiTransferRecordVO;
import com.ruoyi.user.service.ApiUserPackSackService;
import com.ruoyi.user.service.ApiUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Api(tags = "玩家背包")
@RestController
@RequestMapping("/api/userPackSack")
public class ApiUserPackSackController extends BaseController {
/**
* 假号(白号 / 机器人)用户 id 子查询:白号 is_blank_account=1,机器人 user_type='03'。
* 与游戏侧/后台统计的口径保持一致(那边普遍用 is_blank_account != 1 AND user_type != '03' 排除假号)。
*/
private static final String FAKE_ACCOUNT_SQL =
"SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03'";
private final ISysConfigService sysConfigService;
private final ApiUserPackSackService userPackSackService;
private final TtUserService userService;
@Autowired
private ApiBoxTransferRecordMapper boxTransferRecordMapper;
@Autowired
private RedisLock redisLock;
@Autowired
private ApiUserService apiUserService;
@Autowired
private TtOrnamentMapper ttOrnamentMapper;
@Autowired
private TtOrnamentsLevelMapper ttOrnamentsLevelMapper;
public ApiUserPackSackController(ISysConfigService sysConfigService,
ApiUserPackSackService userPackSackService,
TtUserService userService) {
this.sysConfigService = sysConfigService;
this.userPackSackService = userPackSackService;
this.userService = userService;
}
@ApiOperation("用户申请提货")
@PostMapping("/delivery")
public R delivery(@RequestBody @Validated DeliveryParam param) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
// 获取用户信息
TtUser ttUser = userService.getById(getUserId());
// 假号(白号 is_blank_account=1 / 机器人 user_type=03)虚假提货:
// 生成已完成提货记录供 cocos 展示、饰品移出背包,不真正发货、后台不展示
if (isFakeAccount(ttUser)) {
return userPackSackService.blankDelivery(param, ttUser);
}
if ("1".equals(ttUser.getDeliveryStatus())) {
return R.fail(false, "您的帐号提货功能已被禁用,请联系管理员!");
}
// 校验安全密码(用户在「安全密码」中单独设置,与登录密码无关)
if (StringUtils.isBlank(ttUser.getSafePassword())) {
return R.fail(false, "请先设置安全密码!");
}
if (!SecurityUtils.matchesPassword(param.getTwoFactorAuth(), ttUser.getSafePassword())) {
return R.fail(false, "安全密码错误,提货失败!");
}
// 未充值的玩家、主播不允许提货
R rechargeCheck = checkWithdrawRecharge(ttUser);
if (rechargeCheck != null) {
return rechargeCheck;
}
return userPackSackService.delivery(param, ttUser);
}
@ApiOperation("分解饰品")
@UpdateUserCache
@PostMapping("/decompose")
public R<Object> decompose(@RequestBody @Validated DecomposeParam param) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
if (!param.getIsAll() && ObjectUtils.isEmpty(param.getPackSackIds())) {
return R.fail("请传入待分解饰品ID");
}
TtUser ttUser = userService.getById(getUserId());
int result = userPackSackService.decompose(param, ttUser);
return R.ok("成功分解" + result + "件饰品");
}
@ApiOperation("饰品分解记录")
//@UpdateUserCache
@PostMapping("/decomposeLog")
public PageDataInfo<TtBoxRecordsDataVO> decomposeLog(@RequestBody @Validated DecomposeLogCondition param) {
param.setUserId(null);
param.setUserId(getUserId().intValue());
// TtUser ttUser = userService.getById(getUserId());
List<TtBoxRecordsDataVO> ttBoxRecords = userPackSackService.decomposeLog(param);
return getPageData(ttBoxRecords);
}
@ApiOperation("玩家背包")
@PostMapping("/getPackSack")
public R<?> getPackSack(@RequestBody @Validated PackSackCondition condition) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail("登录过期。");
}
// 根据背包类型设置查询条件
Integer packType = condition.getPackType();
java.util.Map<String, Object> result = new java.util.HashMap<>();
if (packType != null && packType == 3) {
// 福利箱子:查询福利表数据(已取消分页)
List<WelfareBoxVO> welfareList = userPackSackService.getWelfareBoxList(userId, null, null);
result.put("total", welfareList == null ? 0 : welfareList.size());
result.put("list", welfareList == null ? java.util.Collections.emptyList() : welfareList);
return R.ok(result);
}
condition.setUidList(Arrays.asList(userId.intValue()));
if (packType != null && packType == 2) {
// 提货背包:状态为申请提货
condition.setStatusList(Arrays.asList(2));
} else {
// 默认我的库存:同时展示「在背包」(0) 与「已申请提货」(2)
condition.setStatusList(Arrays.asList(0, 2));
}
// 我的库存/提货背包取消分页,一次返回全量,以免前端分页加载逻辑 bug 造成与顶部统计不一致
PageUtils.clearPage();
List<UserPackSackDataVO> packSackData = userPackSackService.clientPackSack(condition);
result.put("total", packSackData == null ? 0 : packSackData.size());
result.put("list", packSackData == null ? java.util.Collections.emptyList() : packSackData);
return R.ok(result);
}
@ApiOperation("玩家背包统计数据")
@PostMapping("/packSackGlobalData")
public R<PackSackGlobalData> packSackGlobalData(@RequestBody(required = false) PackSackCondition condition) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail("登录过期。");
}
Integer packType = (condition == null ? null : condition.getPackType());
return userPackSackService.packSackGlobalData(userId.intValue(), packType);
}
@ApiOperation("饰品转赠")
@PostMapping({"/transfer"})
public R<Object> transfer(@RequestBody @Validated TransferParam param) {
TtUser ttUser = this.userService.getById(this.getUserId());
// 被禁用的用户不允许转赠
if (!"0".equals(ttUser.getStatus())) {
return R.fail("账号已被禁用,无法转赠");
}
// 假号(白号 / 机器人)虚假转赠:仅写转赠记录供 cocos 展示,不真正转移饰品,副号接收方查询会排除假号来源
if (isFakeAccount(ttUser)) {
String blankResult = this.userPackSackService.blankTransfer(param, ttUser);
return StringUtils.isNotBlank(blankResult) ? R.fail(blankResult) : R.ok("转赠成功!");
}
// 未充值的玩家、主播不允许转赠
R rechargeCheck = checkWithdrawRecharge(ttUser);
if (rechargeCheck != null) {
return rechargeCheck;
}
// 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 事务,解锁晚于提交,
// 避免弱网重复提交导致重复生成转赠申请(锁粒度=单用户,不影响其他用户)
String lockKey = RedisConstants.TRANSFER_LOCK + ttUser.getUserId();
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
return R.fail("转赠进行中,请勿重复操作");
}
String result;
try {
result = this.userPackSackService.transfer(param, ttUser);
} finally {
redisLock.unlock(lockKey);
}
if (StringUtils.isBlank(result)) {
return R.ok("转赠成功!");
} else if (result.startsWith("PENDING:")) {
return R.ok(result.substring(8));
} else {
return R.fail(result);
}
}
/**
* 是否为假号(白号 / 机器人):假号不参与真实资金链路 —— 提货/转赠一律走虚假流程
* (只生成供 cocos 展示的记录,不发货、不转移饰品归属)。
* 白号判定:is_blank_account=1;机器人判定:user_type='03'(与其它模块的假号口径一致)。
*/
private static boolean isFakeAccount(TtUser ttUser) {
if (ttUser == null) return false;
return Integer.valueOf(1).equals(ttUser.getIsBlankAccount())
|| UserType.ROBOT_USER.getCode().equals(ttUser.getUserType());
}
/**
* 提货/转赠充值校验:仅对玩家、主播校验,未充值则不允许操作。
* 校验通过返回 null,未通过返回错误 R(沿用 checkRecharge 的 703 业务码)。
*/
private R checkWithdrawRecharge(TtUser ttUser) {
String userType = ttUser.getUserType();
if (UserType.COMMON_USER.getCode().equals(userType) || UserType.ANCHOR.getCode().equals(userType)) {
R<Void> rechargeCheck = apiUserService.checkRecharge(ttUser.getUserId());
if (rechargeCheck.getCode() != R.SUCCESS) {
return R.fail(rechargeCheck.getCode(), "未充值用户不允许提货、转赠");
}
}
return null;
}
@ApiOperation("饰品转赠记录")
@PostMapping({"/transfer/records"})
public R<QueryTransferRecordResponse> queryTransferRecords(@RequestBody QueryTransferRecordRequest param) {
if (param.getPageNum() == null) {
param.setPageNum(1);
}
if (param.getPageSize() == null) {
param.setPageSize(10);
}
var userId = this.getUserId();
Page<BoxTransferRecord> page = Page.of(param.getPageNum(), param.getPageSize());
var query = new LambdaQueryChainWrapper<>(this.boxTransferRecordMapper);
if (Objects.equals("src", param.getKind())) {
query.eq(BoxTransferRecord::getSrcUid, userId);
} else {
query.eq(BoxTransferRecord::getDstUid, userId);
// 副号接收方不展示来自假号(白号 / 机器人)的转赠记录(假转赠)
query.notInSql(BoxTransferRecord::getSrcUid, FAKE_ACCOUNT_SQL);
}
var pageData = query.page(page);
var records = pageData.getRecords().stream().map(v -> {
ApiTransferRecordVO vo = new ApiTransferRecordVO();
BeanUtils.copyProperties(v, vo);
vo.setId(v.getId());
vo.setSrcUid(v.getSrcUid());
vo.setDstUid(v.getDstUid());
vo.setRecordsId(v.getBoxRecordId());
vo.setOrnamentImg(v.getImageUrl());
// 显式设置状态字段
vo.setStatus(v.getStatus());
// BoxTransferRecord.createTime 为 LocalDateTime,需转成 Date 避免 Jackson 无 JSR310 模块导致序列化报错
if (v.getCreateTime() != null) {
vo.setCreateTime(java.util.Date.from(v.getCreateTime()
.atZone(java.time.ZoneId.systemDefault()).toInstant()));
}
// 补全:数据中饰品名称/图片/等级图片为空时,通过 ornamentId 查询再去补全
if (v.getOrnamentId() != null &&
(StringUtils.isBlank(v.getOrnamentName()) || StringUtils.isBlank(v.getImageUrl()))) {
TtOrnament ornament = new LambdaQueryChainWrapper<>(ttOrnamentMapper)
.eq(TtOrnament::getId, v.getOrnamentId())
.one();
if (ornament != null) {
if (StringUtils.isBlank(v.getOrnamentName())) {
vo.setOrnamentName(ornament.getShortName());
}
if (StringUtils.isBlank(v.getImageUrl())) {
vo.setOrnamentImg(ornament.getImageUrl());
}
if (StringUtils.isBlank(v.getMarketHashName())) {
vo.setMarketHashName(ornament.getMarketHashName());
}
// 补全等级图片:tt_ornament.exterior 对应 tt_ornaments_level.id
if (StringUtils.isBlank(v.getOrnamentLevelImg()) && StringUtils.isNotEmpty(ornament.getExterior())) {
Integer levelId = Integer.valueOf(ornament.getExterior());
TtOrnamentsLevel level = new LambdaQueryChainWrapper<>(ttOrnamentsLevelMapper)
.eq(TtOrnamentsLevel::getId, levelId)
.one();
if (level != null) {
vo.setOrnamentLevelImg(level.getLevelImg());
}
}
}
}
return vo;
}).collect(Collectors.toList());
QueryTransferRecordResponse response = new QueryTransferRecordResponse();
response.setTotal(pageData.getTotal());
response.setRows(records);
return R.ok(response);
}
}
@@ -0,0 +1,43 @@
package com.ruoyi.user.controller;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest;
import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse;
import com.ruoyi.user.service.ApiProfileRecordsService;
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.validation.annotation.Validated;
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;
/**
* 个人资产详情 - 我的记录 聚合接口
* 一个接口承载 4 个 Tab:余额/充值/提货/赠送。
* 前端 4 个 Tab 共用同一份请求/响应类型,切换 tab 只改 tab 与 subFilter。
*/
@Api(tags = "个人资产详情-我的记录")
@Slf4j
@RestController
@RequestMapping("/api/user/profile")
public class ApiUserProfileRecordsController extends BaseController {
@Autowired
private ApiProfileRecordsService profileRecordsService;
@ApiOperation("我的记录 聚合查询(余额/充值/提货/赠送)")
@PostMapping("/records")
public R<ProfileRecordsResponse> queryRecords(@RequestBody @Validated ProfileRecordsRequest request) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail(401, "登录过期,请重新登录。");
}
ProfileRecordsResponse response = profileRecordsService.queryRecords(userId.intValue(), request);
return R.ok(response);
}
}
@@ -0,0 +1,99 @@
package com.ruoyi.user.controller;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.TtUserSubAccount;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.user.service.ApiUserSubAccountService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Api(tags = "副号管理")
@RestController
@RequestMapping("/api/subAccount")
public class ApiUserSubAccountController extends BaseController {
@Autowired
private ApiUserSubAccountService subAccountService;
@Autowired
private TtUserService userService;
@Data
public static class BindSubAccountParam {
@NotNull(message = "副号ID不能为空")
private Long subUid;
}
@Data
public static class SubAccountVO {
private Long id;
private Long subUid;
private String nickName;
private String avatar;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
@ApiOperation("绑定副号")
@PostMapping("/bind")
public R<Void> bindSubAccount(@RequestBody BindSubAccountParam param) {
Long mainUid = getUserId();
return subAccountService.bindSubAccount(mainUid, param.getSubUid());
}
@ApiOperation("解绑副号")
@PostMapping("/unbind")
public R<Void> unbindSubAccount(@RequestBody BindSubAccountParam param) {
Long mainUid = getUserId();
return subAccountService.unbindSubAccount(mainUid, param.getSubUid());
}
@ApiOperation("查询已绑定的副号列表")
@PostMapping("/list")
public R<List<SubAccountVO>> getSubAccounts() {
Long mainUid = getUserId();
R<List<TtUserSubAccount>> result = subAccountService.getSubAccounts(mainUid);
if (!result.getCode().equals(200)) {
return R.fail(result.getMsg());
}
// 未绑定任何副号时,直接返回空列表,避免后续批量查询触发异常
List<TtUserSubAccount> records = result.getData();
if (records == null || records.isEmpty()) {
return R.ok(Collections.emptyList());
}
// 批量查询副号用户信息,避免 N+1 问题
List<Long> subUidList = records.stream()
.map(TtUserSubAccount::getSubUid)
.collect(Collectors.toList());
Map<Integer, TtUser> userMap = userService.listByIds(subUidList).stream()
.collect(Collectors.toMap(TtUser::getUserId, Function.identity()));
List<SubAccountVO> voList = records.stream().map(record -> {
SubAccountVO vo = new SubAccountVO();
vo.setId(record.getId());
vo.setSubUid(record.getSubUid());
vo.setCreateTime(record.getCreateTime());
TtUser subUser = userMap.get(record.getSubUid().intValue());
if (subUser != null) {
vo.setNickName(subUser.getNickName());
vo.setAvatar(subUser.getAvatar());
}
return vo;
}).collect(Collectors.toList());
return R.ok(voList);
}
}
@@ -0,0 +1,73 @@
package com.ruoyi.user.controller;
import com.ruoyi.domain.other.TtBanner;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.contract.api.request.ContentByTypeRequest;
import com.ruoyi.user.service.ApiWebsiteSetupService;
import com.ruoyi.domain.vo.ApiContentDataVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Api(tags = "网站设置")
@RestController
@RequestMapping("/api/websiteSetup")
public class ApiWebsiteSetupController {
private final ApiWebsiteSetupService apiWebsiteSetupService;
private final ISysConfigService sysConfigService;
public ApiWebsiteSetupController(ApiWebsiteSetupService apiWebsiteSetupService,
ISysConfigService sysConfigService) {
this.apiWebsiteSetupService = apiWebsiteSetupService;
this.sysConfigService = sysConfigService;
}
@ApiOperation("获取横幅列表")
@Anonymous
@PostMapping("/getBannerList")
public R<List<TtBanner>> getBannerList() {
List<TtBanner> list = apiWebsiteSetupService.getBannerList();
return R.ok(list);
}
@ApiOperation("获取类型内容")
@PostMapping("/getContentByType")
@Anonymous
public R<ApiContentDataVO> getContentByType(@RequestBody ContentByTypeRequest request) {
ApiContentDataVO data = apiWebsiteSetupService.getContentByType(request.getAlias());
return R.ok(data);
}
/**
* 全站维护状态查询。
* <p>供前端启动时/轮询调用:status=1 正常访问;status=0 全站维护中(前端展示全站维护页面)。
* <p>响应禁用浏览器缓存,即使前端存在缓存也能实时拿到最新维护状态。
*/
@ApiOperation("获取全站维护状态")
@Anonymous
@PostMapping("/siteMaintenance")
public R<Map<String, Object>> getSiteMaintenanceStatus(HttpServletResponse response) {
// 禁用缓存,保证维护开关实时生效
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0L);
// sys_config 中 websiteMaintenance:"1"=维护中,"0"/其它=正常
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
boolean maintaining = "1".equals(websiteMaintenance);
Map<String, Object> data = new HashMap<>(4);
// status:0=全站维护中(展示维护页),1=正常访问
data.put("status", maintaining ? 0 : 1);
data.put("message", maintaining ? "全站维护中,请稍后再试" : "");
return R.ok(data);
}
}
@@ -0,0 +1,18 @@
package com.ruoyi.user.mapper;
import com.ruoyi.domain.other.TtAnnouncement;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiAnnouncementMapper {
List<TtAnnouncement> getAnnouncementList(Long userId);
List<TtAnnouncement> getSystemAnnouncementList();
TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId);
int countUnreadAnnouncement(Long userId);
}
@@ -0,0 +1,12 @@
package com.ruoyi.user.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface ApiAnnouncementReadMapper {
int countAnnouncementRead(@Param("announcementId") Integer announcementId, @Param("userId") Long userId);
int addAnnouncementRead(@Param("announcementId") Integer announcementId, @Param("userId") Long userId);
}
@@ -0,0 +1,9 @@
package com.ruoyi.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.BoxTransferRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiBoxTransferRecordMapper extends BaseMapper<BoxTransferRecord> {
}
@@ -0,0 +1,12 @@
package com.ruoyi.user.mapper;
import com.ruoyi.domain.vo.ApiMessageDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiMessageMapper {
List<ApiMessageDataVO> getMessageList(@Param("userId") Long userId, @Param("id") Integer id);
}
@@ -0,0 +1,35 @@
package com.ruoyi.user.mapper;
import com.ruoyi.domain.other.TtNotice;
import com.ruoyi.user.model.vo.ApiNoticeVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiNoticeMapper {
List<ApiNoticeVO> getNoticeList(Long userId);
List<ApiNoticeVO> getNoticeListByCategoryPrefix(@Param("userId") Long userId, @Param("categoryPrefix") String categoryPrefix);
ApiNoticeVO getNoticeByNoticeId(@Param("userId") Long userId, @Param("noticeId") Integer noticeId);
int countUnreadNotice(Long userId);
int addNotice(TtNotice ttNotice);
int editNotice(TtNotice ttNotice);
int removeNoticeByNoticeId(Integer noticeId);
/**
* 批量设置指定分类的通知为已读
*
* @param userId 用户 ID
* @param categoryPrefix 分类前缀(GAME_表示游戏通知,SYS_表示系统通知)
* @return 影响行数
*/
int markNoticesAsReadByCategory(@Param("userId") Long userId, @Param("categoryPrefix") String categoryPrefix);
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.mapper;
import com.ruoyi.domain.other.ApiShoppingBody;
import com.ruoyi.domain.vo.ApiShoppingDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiShoppingMapper {
List<ApiShoppingDataVO> list(@Param("shoppingBody") ApiShoppingBody shoppingBody, @Param("exchangePriceRatio") Integer exchangePriceRatio);
}
@@ -0,0 +1,31 @@
package com.ruoyi.user.mapper;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.user.model.TtTaskCenterUser;
import com.ruoyi.user.model.dto.YesterdayExpenditureDTO;
import com.ruoyi.user.model.vo.ApiTaskCenterVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface ApiTaskCenterMapper {
List<ApiTaskCenterVO> selectApiTaskCenterVOList(Integer userId);
List<YesterdayExpenditureDTO> getYesterdayExpenditure();
BigDecimal selectCreditByTaskIdAndUserId(@Param("taskId") Integer taskId, @Param("userId") Integer userId);
TtTaskCenterUser selectTtTaskCenterUserByTaskIdAndUserId(@Param("taskId") Integer taskId, @Param("userId") Integer userId);
int insertYesterdayExpenditureBonusPoints(List<TtTaskCenterUser> ttTaskCenterUserList);
int deleteYesterdayExpenditureBonusPoints(Integer taskId);
int markAsClaimedByUserIdAndType(@Param("taskId") Integer taskId, @Param("userId") Integer userId);
int insertTaskCenterRecord(TtTaskCenterUser ttTaskCenterUser);
}
@@ -0,0 +1,52 @@
package com.ruoyi.user.mapper;
import com.ruoyi.user.model.vo.ApiConsumeRankingVO;
import com.ruoyi.user.model.vo.ApiRechargeRankingVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface ApiUserBlendErcashMapper {
/**
* 获取指定日期的消费排行榜
*/
List<ApiRechargeRankingVO> getRechargeRankingByDate(String date);
/**
* 今日消费流水 Top10。
* 统计口径:tt_user_blend_ercash 中 type=0 (OUTPUT) 且 source 命中
* {@link com.ruoyi.domain.common.constant.TtAccountRecordSource#getGameBetCodes()},
* 按用户聚合后按总消费降序,排除机器人 (user_type='03')。
*
* @param beginT 今日 00:00:00
* @param endT 今日 23:59:59
* @param sourceList 计入消费的 source 集合
* @param limit 返回条数(默认 10)
*/
List<ApiConsumeRankingVO> topConsumeRanking(@Param("beginT") String beginT,
@Param("endT") String endT,
@Param("sourceList") List<Integer> sourceList,
@Param("limit") Integer limit);
/**
* 查询指定用户在 [beginT, endT] 区间内、命中 sourceList 的累计消费(绝对值)。
* 没有任何流水时返回 0。
*/
BigDecimal userConsumeTotal(@Param("userId") Long userId,
@Param("beginT") String beginT,
@Param("endT") String endT,
@Param("sourceList") List<Integer> sourceList);
/**
* 统计在同样口径下消费总额严格大于 myTotal 的用户数(用于推算个人排名)。
* 排名 = countUsersAheadOf(myTotal) + 1。
*/
Integer countUsersAheadOf(@Param("myTotal") BigDecimal myTotal,
@Param("beginT") String beginT,
@Param("endT") String endT,
@Param("sourceList") List<Integer> sourceList);
}
@@ -0,0 +1,33 @@
package com.ruoyi.user.mapper;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import com.ruoyi.domain.vo.client.PackSackGlobalData;
import com.ruoyi.domain.vo.client.WelfareBoxVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiUserPackSackMapper {
List<UserPackSackDataVO> getPackSack(Integer userId);
List<UserPackSackDataVO> clientPackSack(
@Param("uidList") List<Integer> uidList,
@Param("statusList") List<Integer> statusList,
@Param("sourceList") List<Integer> sourceList,
@Param("name") String name,
@Param("beginTime") String beginTime,
@Param("endTime") String endTime,
@Param("priceSort") Integer priceSort,
@Param("timeSort") Integer timeSort);
PackSackGlobalData packSackGlobalData(@Param("userId") Integer userId,
@Param("statusList") List<Integer> statusList);
PackSackGlobalData welfareBoxGlobalData(@Param("userId") Integer userId);
List<WelfareBoxVO> getWelfareBoxList(@Param("userId") Long userId,
@Param("limit") Integer limit,
@Param("size") Integer size);
}
@@ -0,0 +1,53 @@
package com.ruoyi.user.mapper;
import org.apache.ibatis.annotations.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;
/**
* 推广CDKEY兑换 Mapper(service-user模块直接访问promo_cdkey/promo_cdkey_redeem表)
*/
@Mapper
public interface PromoCdkeyUserMapper {
/**
* 根据CDKEY码查询进行中的CDKEY
*/
@Select("SELECT id, title, cdkey_code, amount_min, amount_max, num, usable_count, " +
"used_count, status, creator_id, creator_name, opening_time, validity, create_time " +
"FROM promo_cdkey WHERE cdkey_code = #{code} AND status = 0 LIMIT 1")
Map<String, Object> findActiveCdkey(@Param("code") String code);
/**
* 递减可用次数
*/
@Update("UPDATE promo_cdkey SET usable_count = usable_count - 1, used_count = used_count + 1 " +
"WHERE id = #{id} AND usable_count > 0")
int decrementUsableCount(@Param("id") Long id);
/**
* 当可用次数为0时更新状态为已结束
*/
@Update("UPDATE promo_cdkey SET status = 1 WHERE id = #{id} AND usable_count <= 0")
int updateStatusIfExhausted(@Param("id") Long id);
/**
* 检查用户是否已兑换过该CDKEY
*/
@Select("SELECT COUNT(*) FROM promo_cdkey_redeem WHERE cdkey_id = #{cdkeyId} AND redeemer_id = #{redeemerId}")
int countUserRedeem(@Param("cdkeyId") Long cdkeyId, @Param("redeemerId") Long redeemerId);
/**
* 插入兑换记录
*/
@Insert("INSERT INTO promo_cdkey_redeem (cdkey_id, cdkey_code, redeemer_id, redeemer_name, unit_value, redeem_time) " +
"VALUES (#{cdkeyId}, #{cdkeyCode}, #{redeemerId}, #{redeemerName}, #{unitValue}, #{redeemTime})")
int insertRedeemRecord(@Param("cdkeyId") Long cdkeyId,
@Param("cdkeyCode") String cdkeyCode,
@Param("redeemerId") Long redeemerId,
@Param("redeemerName") String redeemerName,
@Param("unitValue") BigDecimal unitValue,
@Param("redeemTime") Date redeemTime);
}
@@ -0,0 +1,16 @@
package com.ruoyi.user.model;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
@Data
public class TransferParam {
@NotEmpty(message = "饰品ID列表不能为空")
private List<Long> packSackIds;
@NotNull(message = "目标用户ID不能为空")
private Integer targetUid;
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.model;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class TtTaskCenterUser {
private Integer taskId;
private Integer userId;
private BigDecimal credit;
private String claimed;
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.model.dto;
import lombok.Data;
@Data
public class RealNameAuthenticationDTO {
private String realName;
private String idNum;
private String certifyId;
}
@@ -0,0 +1,11 @@
package com.ruoyi.user.model.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class YesterdayExpenditureDTO {
private Integer userId;
private BigDecimal totalRecharge;
}
@@ -0,0 +1,40 @@
package com.ruoyi.user.model.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 今日消费流水排行榜(Top10 + 个人排名)共享 VO
* <p>
* - Top10 接口:rank 为 "1"~"10"
* - 个人排名接口:rank 命中 Top10 时为 "1"~"10",未命中(排名超过 10 或今日无消费)时为 "-"
* - 奖励:来自后台 tt_recharge_ranking_reward;个人排名超过 10 时 reward = 0
* - 总消费流水统计口径:参考 {@link com.ruoyi.domain.common.constant.TtAccountRecordSource#getGameBetCodes()}
*/
@Data
public class ApiConsumeRankingVO {
/** 排名:"1"~"10";个人未上榜时为 "-" */
private String rank;
private Long userId;
/** 昵称 */
private String nickName;
/** 头像 */
private String avatar;
/** 今日总消费流水(绝对值) */
private BigDecimal totalConsume;
/** 今日总消费流水(格式化字符串) */
private String totalConsumeStr;
/** 排名奖励金额 */
private BigDecimal reward;
/** 排名奖励金额(格式化字符串) */
private String rewardStr;
}
@@ -0,0 +1,23 @@
package com.ruoyi.user.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
@Data
public class ApiNoticeVO {
private Integer noticeId;
private String title;
private String content;
private String category;
private String read;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,25 @@
package com.ruoyi.user.model.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ApiRechargeRankingVO {
private Integer ranking;
private Long userId;
private String nickName;
private String avatar;
/** 金币 */
private BigDecimal amount;
/** 积分 */
private BigDecimal credits;
private BigDecimal totalRecharge;
}
@@ -0,0 +1,23 @@
package com.ruoyi.user.model.vo;
import lombok.Data;
@Data
public class ApiTaskCenterVO {
private Integer taskId;
private String taskName;
private String description;
/**
* 领取条件(0不满足 1满足)
*/
private String status;
/**
* 是否已领取(1已领取)
*/
private String claimed;
}
@@ -0,0 +1,46 @@
package com.ruoyi.user.model.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class ApiTransferRecordVO {
@TableId
private Long id;
private Long srcUid;
private Long dstUid;
private Long recordsId;
private Long ornamentId;
private String ornamentName;
private String ornamentImg;
private String ornamentLevelImg;
private String marketHashName;
private BigDecimal ornamentsPrice;
/**
* 转赠状态:0-审核中,1-成功,2-失败
*/
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private Date createTime;
}
@@ -0,0 +1,41 @@
package com.ruoyi.user.model.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 公告/通知统一展示项
*
* 用于 /api/announcement/list 与 /api/announcement/detail 接口,
* 将「公告(tt_announcement)」与「用户通知(tt_notice)」合并为同一种数据结构返回。
*/
@Data
public class MessageItemVO {
/** 主键ID */
private Integer id;
/** 数据来源:1 公告 / 2 通知 */
private Integer source;
/** 标题 */
private String title;
/** 内容 */
private String content;
/** 消息分类:GAME_xxx 或 SYS_xxx */
private String category;
/** Tab 类型:0 系统 / 1 游戏 */
private Integer tabType;
/** 阅读状态:0 未读 / 1 已读 */
private String read;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -0,0 +1,16 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.other.TtAnnouncement;
import java.util.List;
public interface ApiAnnouncementService {
List<TtAnnouncement> getAnnouncementList(Long userId);
List<TtAnnouncement> getSystemAnnouncementList();
TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId, Long userId);
int countUnreadAnnouncement(Long userId);
}
@@ -0,0 +1,15 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.user.contract.api.response.QueryVipRewardResponse;
import com.ruoyi.user.contract.api.response.VipFlowRewardResponse;
public interface ApiBonusService {
R receiveRedPacket(String code, TtUser ttUser);
R<QueryVipRewardResponse> vipRewardList(TtUser ttUser);
R<VipFlowRewardResponse> vipFlowReward(TtUser ttUser);
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.AjaxResult;
import org.apache.commons.lang3.tuple.Pair;
public interface ApiLoginService {
Pair<String, AjaxResult> login(String username, String password);
AjaxResult verificationCodeLogin(String phoneNumber, String code);
AjaxResult verificationCodeLoginOrRegister(String phoneNumber, String code, String parentInvitationCode);
}
@@ -0,0 +1,14 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.vo.ApiMessageDataVO;
import java.util.List;
public interface ApiMessageService {
List<ApiMessageDataVO> getMessageList(Long userId, Integer id);
ApiMessageDataVO view(Long userId, Integer id);
String batchOperation(Long userId, List<Integer> ids, String status);
}
@@ -0,0 +1,31 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.other.TtNotice;
import com.ruoyi.user.model.vo.ApiNoticeVO;
import java.util.List;
public interface ApiNoticeService {
List<ApiNoticeVO> getNoticeList(Long userId);
List<ApiNoticeVO> getNoticeListByCategoryPrefix(Long userId, String categoryPrefix);
ApiNoticeVO getNoticeByNoticeId(Long userId, Integer noticeId);
int countUnreadNotice(Long userId);
int addNotice(TtNotice ttNotice);
int editNotice(TtNotice ttNotice);
int removeNoticeByNoticeId(Integer noticeId);
/**
* 批量设置指定分类的通知为已读
*
* @param categoryPrefix 分类前缀(GAME_表示游戏通知,SYS_表示系统通知)
* @return 影响行数
*/
int markNoticesAsReadByCategory(String categoryPrefix);
}
@@ -0,0 +1,19 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest;
import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse;
/**
* 个人资产详情 - 我的记录 聚合查询服务
* 一个接口承载 4 个 Tab:余额/充值/提货/赠送
*/
public interface ApiProfileRecordsService {
/**
* 统一查询入口
*
* @param userId 当前登录用户ID
* @param request 请求参数
*/
ProfileRecordsResponse queryRecords(Integer userId, ProfileRecordsRequest request);
}
@@ -0,0 +1,10 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo;
import java.util.List;
public interface ApiPromotionRecordService {
List<TtPromotionRecordVo> getPromotionRecord(Integer userId);
}
@@ -0,0 +1,8 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.other.ApiRegisterBody;
public interface ApiRegisterService {
String register(ApiRegisterBody registerBody);
}
@@ -0,0 +1,25 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.vo.share.ShareCenterVO;
import com.ruoyi.domain.vo.share.ShareRecordPageVO;
/**
* 主页"游戏分享"模块服务接口
*/
public interface ApiShareService {
/**
* 获取分享中心顶部数据(邀请码、今日/累计收益、邀请人数、分享链接)
*/
ShareCenterVO getShareCenter(Long userId);
/**
* 分页查询分享记录列表(含按下级用户ID搜索)
*
* @param userId 当前登录用户ID(即邀请人)
* @param subUserId 可选的下级用户ID过滤;为 null 或 <=0 表示不过滤
* @param pageNum 页码,从 1 开始
* @param pageSize 每页大小
*/
ShareRecordPageVO listShareRecords(Long userId, Long subUserId, int pageNum, int pageSize);
}
@@ -0,0 +1,25 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiShoppingBody;
import com.ruoyi.domain.vo.ApiShoppingDataVO;
import java.math.BigDecimal;
import java.util.List;
public interface ApiShoppingService {
/**
* 商城商品列表
* @param shoppingBody 查询条件(含常规过滤与 isExchange)
* @param userCredits 当前登录用户的积分;未登录传 null。
* 仅在 isExchange=true 时生效,用于过滤出当前积分可兑换的饰品
* 过滤公式:use_price * exchangePriceRatio <= userCredits。
*/
List<ApiShoppingDataVO> list(ApiShoppingBody shoppingBody, BigDecimal userCredits);
R exchange(TtUser ttUser, List<Long> ornamentsIds);
String integratingConversion(TtUser ttUser, BigDecimal credits);
}
@@ -0,0 +1,18 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.user.model.TtTaskCenterUser;
import com.ruoyi.user.model.vo.ApiTaskCenterVO;
import java.util.List;
public interface ApiTaskCenterService {
List<ApiTaskCenterVO> selectApiTaskCenterVOList(Integer userId);
TtTaskCenterUser selectTtTaskCenterUserByTaskIdAndUserId(Integer taskId, Integer userId);
void updateYesterdayBonusPoints();
AjaxResult getReward(Integer taskId, Integer userId);
}
@@ -0,0 +1,46 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.packSack.DecomposeLogCondition;
import com.ruoyi.domain.dto.packSack.DecomposeParam;
import com.ruoyi.domain.dto.packSack.DeliveryParam;
import com.ruoyi.domain.dto.packSack.PackSackCondition;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.vo.TtBoxRecordsDataVO;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import com.ruoyi.domain.vo.client.PackSackGlobalData;
import com.ruoyi.domain.vo.client.WelfareBoxVO;
import com.ruoyi.user.model.TransferParam;
import java.util.List;
public interface ApiUserPackSackService {
R delivery(DeliveryParam param, TtUser ttUser);
/**
* 白号假提货:生成已完成的提货记录供 cocos 展示,将饰品移出背包,不真正发货。
*/
R blankDelivery(DeliveryParam param, TtUser ttUser);
int decompose(DecomposeParam param, TtUser ttUser);
List<UserPackSackDataVO> getPackSack(Integer userId);
List<TtBoxRecords> packSackHandle(List<Long> packSackIds, TtUser ttUser, Integer status);
List<TtBoxRecordsDataVO> decomposeLog(DecomposeLogCondition param);
List<UserPackSackDataVO> clientPackSack(PackSackCondition condition);
R<PackSackGlobalData> packSackGlobalData(Integer userId, Integer packType);
String transfer(TransferParam param, TtUser ttUser);
/**
* 白号假转赠:生成转赠记录供 cocos 展示,不真正转移饰品到副号背包。
*/
String blankTransfer(TransferParam param, TtUser ttUser);
List<WelfareBoxVO> getWelfareBoxList(Long userId, Integer page, Integer size);
}
@@ -0,0 +1,36 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiForgetPasswordBody;
import com.ruoyi.domain.other.ApiSafePasswordBody;
import com.ruoyi.domain.other.ApiUpdateUserDetailsBody;
import com.ruoyi.domain.other.RealNameAuthenticationBody;
import org.springframework.web.multipart.MultipartFile;
public interface ApiUserService {
String profilePictureUpload(TtUser ttUser, MultipartFile file);
String updateUserDetails(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody);
String forgetPassword(ApiForgetPasswordBody apiForgetPasswordBody);
String realNameAuthentication(TtUser ttUser, RealNameAuthenticationBody realNameAuthenticationBody);
String realNameAuthentication2(TtUser ttUser, String realName, String idNum, String phoneNum, String captcha);
String authenticationOk(TtUser ttUser);
R<Object> changePW(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody,String token);
R<Object> setSafePassword(TtUser ttUser, ApiSafePasswordBody apiSafePasswordBody);
R bindBoss(TtUser ttUser, ApiUpdateUserDetailsBody updateUserDetailsBody);
Integer getSubordinateNumber(Long userId);
R<Void> checkRecharge(Integer userId);
R bindInvitationCode(TtUser ttUser, String invitationCode);
}
@@ -0,0 +1,29 @@
package com.ruoyi.user.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.entity.TtUserSubAccount;
import java.util.List;
public interface ApiUserSubAccountService {
/**
* 绑定副号
*/
R<Void> bindSubAccount(Long mainUid, Long subUid);
/**
* 解绑副号
*/
R<Void> unbindSubAccount(Long mainUid, Long subUid);
/**
* 查询主号绑定的所有副号
*/
R<List<TtUserSubAccount>> getSubAccounts(Long mainUid);
/**
* 校验目标用户是否是当前用户的副号
*/
boolean isSubAccount(Long mainUid, Long subUid);
}
@@ -0,0 +1,13 @@
package com.ruoyi.user.service;
import com.ruoyi.domain.other.TtBanner;
import com.ruoyi.domain.vo.ApiContentDataVO;
import java.util.List;
public interface ApiWebsiteSetupService {
List<TtBanner> getBannerList();
ApiContentDataVO getContentByType(String alias);
}
@@ -0,0 +1,44 @@
package com.ruoyi.user.service.impl;
import com.ruoyi.domain.other.TtAnnouncement;
import com.ruoyi.user.mapper.ApiAnnouncementMapper;
import com.ruoyi.user.mapper.ApiAnnouncementReadMapper;
import com.ruoyi.user.service.ApiAnnouncementService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ApiAnnouncementServiceImpl implements ApiAnnouncementService {
@Autowired
private ApiAnnouncementMapper apiAnnouncementMapper;
@Autowired
private ApiAnnouncementReadMapper apiAnnouncementReadMapper;
@Override
public List<TtAnnouncement> getAnnouncementList(Long userId) {
return apiAnnouncementMapper.getAnnouncementList(userId);
}
@Override
public List<TtAnnouncement> getSystemAnnouncementList() {
return apiAnnouncementMapper.getSystemAnnouncementList();
}
@Override
public TtAnnouncement getAnnouncementByAnnouncementId(Integer announcementId, Long userId) {
// 新增用户和公告关联,表示已读
if (apiAnnouncementReadMapper.countAnnouncementRead(announcementId, userId) == 0) {
apiAnnouncementReadMapper.addAnnouncementRead(announcementId, userId);
}
return apiAnnouncementMapper.getAnnouncementByAnnouncementId(announcementId);
}
@Override
public int countUnreadAnnouncement(Long userId) {
return apiAnnouncementMapper.countUnreadAnnouncement(userId);
}
}
@@ -0,0 +1,666 @@
package com.ruoyi.user.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.mapper.TtRedPacketMapper;
import com.ruoyi.admin.mapper.TtRedPacketRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.service.TtUserTransferDailyService;
import com.ruoyi.admin.service.TtVipLevelService;
import com.ruoyi.admin.util.RandomUtils;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MoneyUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.entity.transfer.TtUserTransferDaily;
import com.ruoyi.domain.other.TtRedPacket;
import com.ruoyi.domain.other.TtRedPacketRecord;
import com.ruoyi.domain.other.TtVipLevel;
import com.ruoyi.domain.vo.TtUserVipProgressVo;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.user.mapper.PromoCdkeyUserMapper;
import com.ruoyi.user.contract.api.response.QueryVipRewardResponse;
import com.ruoyi.user.contract.api.response.VipFlowRewardResponse;
import com.ruoyi.user.service.ApiBonusService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class ApiBonusServiceImpl implements ApiBonusService {
private final TtUserService userService;
private final TtRedPacketMapper redPacketMapper;
private final TtRedPacketRecordMapper redPacketRecordMapper;
private final RedisLock redisLock;
public ApiBonusServiceImpl(TtUserService userService,
TtRedPacketMapper redPacketMapper,
TtRedPacketRecordMapper redPacketRecordMapper,
RedisLock redisLock) {
this.userService = userService;
this.redPacketMapper = redPacketMapper;
this.redPacketRecordMapper = redPacketRecordMapper;
this.redisLock = redisLock;
}
@Autowired
private TtUserBlendErcashMapper userBlendErcashMapper;
@Autowired
private TtVipLevelService vipLevelService;
@Autowired
private TtUserTransferDailyService userTransferDailyService;
@Autowired
private PromoCdkeyUserMapper promoCdkeyUserMapper;
/**
* 归一化玩家粘贴进来的口令。
* <p>
* 后台「亏损返利」站内信把口令写成「兑换CDK口令:[XXXX]」,那对方括号是给客户端切口令用的标记。
* 玩家很可能连括号一起复制粘贴过来,这里统一去掉,避免因为两个符号导致兑换失败
* (tt_red_packet.password 与 promo_cdkey.cdkey_code 本身都不含方括号,去掉无副作用)。
*/
private String normalizeCode(String code) {
if (StringUtils.isBlank(code)) return code;
return code.trim().replace("[", "").replace("]", "");
}
// 领红包检查
private R<TtRedPacket> receiveRedPacketCheck(String code, TtUser ttUser) {
if (StringUtils.isBlank(code)) return R.fail("口令错误。");
TtRedPacket ttRedPacket = new LambdaQueryChainWrapper<>(redPacketMapper)
.eq(TtRedPacket::getPassword, code)
.eq(TtRedPacket::getDelFlag, 0)
.one();
if (ObjectUtil.isNull(ttRedPacket)) return R.fail("口令错误,没有合法的红包。");
if (ttRedPacket.getStatus().equals(1)) return R.fail("红包已经抢光了。");
List<TtRedPacketRecord> allRedPackRecord = new LambdaQueryChainWrapper<>(redPacketRecordMapper)
.eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId())
.list();
if (allRedPackRecord.size() >= ttRedPacket.getNum()) return R.fail("红包已经抢光了!");
if (Objects.isNull(ttRedPacket.getUseStatus()) || ttRedPacket.getUseStatus().equals(1))
return R.fail("该红包被禁用,请联系管理员。");
// 指定领取人:非空时只允许本人领取(亏损返利CDK 用该字段绑定收款人,
// 避免口令被截图/转发后别人先兑走)。为空则是原有的"不限用户"行为。
Integer receiverUserId = ttRedPacket.getReceiverUserId();
if (receiverUserId != null && receiverUserId > 0
&& !Objects.equals(receiverUserId, ttUser.getUserId())) {
log.info("CDKEY兑换失败: userId={}, code={}, 原因=非指定领取人 receiverUserId={}",
ttUser.getUserId(), code, receiverUserId);
return R.fail("该CDK仅限本人领取。");
}
if (ObjectUtil.isNotEmpty(ttRedPacket.getUserId()) && Objects.equals(ttUser.getUserId(), ttRedPacket.getUserId()))
return R.fail("给粉丝的专属红包,您自己无法领取。");
if (ObjectUtil.isNotEmpty(ttRedPacket.getUserId())
&& ttRedPacket.getUserId() != 0
&& !Objects.equals(ttUser.getParentId(), ttRedPacket.getUserId()))
return R.fail("粉丝专属红包,您尚未关注该主播。");
Date openingTime = ttRedPacket.getOpeningTime();
if (DateUtils.getNowDate().compareTo(ttRedPacket.getOpeningTime()) < 0)
return R.fail("红包开启时间:" + DateUtil.format(openingTime, "yyyy-MM-dd HH:mm:ss") + ",敬请期待。");
Date validity = ttRedPacket.getValidity();
if (StringUtils.isNotNull(validity) && DateUtils.getNowDate().compareTo(validity) > 0) {
ttRedPacket.setStatus(1);
new LambdaUpdateChainWrapper<>(redPacketMapper)
.eq(TtRedPacket::getId, ttRedPacket.getId())
.set(TtRedPacket::getStatus, 1)
.update();
return R.fail("红包已结束");
}
boolean alreadyReceived = allRedPackRecord.stream()
.anyMatch(r -> r.getUserId().equals(ttUser.getUserId()));
if (alreadyReceived) {
return R.fail("您已领取过该红包,请勿重复领取。");
}
return R.ok(ttRedPacket);
}
@Override
public R receiveRedPacket(String code, TtUser ttUser) {
code = normalizeCode(code);
log.info("receiveRedPacket: userId={}, code={}", ttUser.getUserId(), code);
R<TtRedPacket> check = receiveRedPacketCheck(code, ttUser);
if (!check.getCode().equals(200)) {
// 红包找不到,尝试从推广CDKEY表兑换
log.info("红包口令未匹配,转入CDKEY兑换: userId={}, code={}, 红包检查结果={}", ttUser.getUserId(), code, check.getMsg());
return tryRedeemPromoCdkey(code, ttUser);
}
TtRedPacket ttRedPacket = check.getData();
// 锁前快速检查:该用户是否已领取该红包(减少锁竞争,非安全校验)
int preCheckCount = new LambdaQueryChainWrapper<>(redPacketRecordMapper)
.eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId())
.eq(TtRedPacketRecord::getUserId, ttUser.getUserId())
.count().intValue();
if (preCheckCount > 0) {
return R.fail("您已领取过该红包,请勿重复领取。");
}
// 用户级锁(锁粒度: redPacketId + userId,防止同一用户并发领取同一红包)
String lockName = RedisConstants.RECEIVE_RED_PACKET_LOCK + ttRedPacket.getId() + ":" + ttUser.getUserId();
boolean lock = redisLock.tryLock(lockName, 0L, 30L, TimeUnit.SECONDS);
if (!lock) return R.fail("没抢到红包。");
// 红包级全局锁(锁粒度: redPacketId,防止不同用户并发领取导致库存超发)
String packetLockName = RedisConstants.RECEIVE_RED_PACKET_LOCK + ttRedPacket.getId();
boolean packetLock = false;
for (int l = 0; l < 5; l++) {
packetLock = redisLock.tryLock(packetLockName, 5L, 10L, TimeUnit.SECONDS);
if (packetLock) break;
}
if (!packetLock) {
redisLock.unlock(lockName);
return R.fail("没抢到红包。");
}
try {
// 锁内二次检查(防TOCTOU竞态条件)
int doubleCheckCount = new LambdaQueryChainWrapper<>(redPacketRecordMapper)
.eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId())
.eq(TtRedPacketRecord::getUserId, ttUser.getUserId())
.count().intValue();
if (doubleCheckCount > 0) {
log.warn("红包重复领取(锁内二次检查拦截): userId={}, redPacketId={}", ttUser.getUserId(), ttRedPacket.getId());
return R.fail("您已领取过该红包,请勿重复领取。");
}
List<TtRedPacketRecord> redPacketRecords = new LambdaQueryChainWrapper<>(redPacketRecordMapper)
.eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId())
.list();
if (ttRedPacket.getNum() > redPacketRecords.size()) {
// 构建抢红包记录
BigDecimal receiveAmount = RandomUtils.getRandomPrice(ttRedPacket.getAmount());
receiveAmount = receiveAmount.setScale(2, RoundingMode.HALF_UP);
TtRedPacketRecord redPacketRecord = new TtRedPacketRecord();
redPacketRecord.setRedPacketId(ttRedPacket.getId());
redPacketRecord.setUserId(ttUser.getUserId());
redPacketRecord.setReceivePassword(code);
redPacketRecord.setReceiveAmount(receiveAmount);
redPacketRecord.setReceiveTime(DateUtils.getNowDate());
// 保存记录(捕获唯一约束异常作为最后防线)
try {
redPacketRecordMapper.insert(redPacketRecord);
} catch (DuplicateKeyException dke) {
log.warn("红包重复领取(唯一约束拦截): userId={}, redPacketId={}", ttUser.getUserId(), ttRedPacket.getId());
return R.fail("您已领取过该红包,请勿重复领取。");
}
// 更新用户账户:userId > 0 为主播创建的CDK,使用推广CDKEY兑换收入;否则为平台福利CDK收入
TtAccountRecordSource redeemSource = (ttRedPacket.getUserId() != null && ttRedPacket.getUserId() > 0)
? TtAccountRecordSource.PROMO_CDKEY_REDEEM
: TtAccountRecordSource.RECEIVE_RED_PACKET;
userService.updateOnlyUserAccount(ttUser.getUserId(), receiveAmount, redeemSource);
// 更新红包状态
Integer count = new LambdaQueryChainWrapper<>(redPacketRecordMapper)
.eq(TtRedPacketRecord::getRedPacketId, ttRedPacket.getId())
.count().intValue();
if (ttRedPacket.getNum() <= count) {
// 抢完更新为已结束
ttRedPacket.setStatus(1);
new LambdaUpdateChainWrapper<>(redPacketMapper)
.eq(TtRedPacket::getId, ttRedPacket.getId())
.set(TtRedPacket::getStatus, 1)
.update();
}
return R.ok(receiveAmount);
} else {
new LambdaUpdateChainWrapper<>(redPacketMapper)
.eq(TtRedPacket::getId, ttRedPacket.getId())
.set(TtRedPacket::getStatus, 1)
.update();
return R.fail("红包已经抢光了!");
}
} catch (Exception e) {
log.error("任务执行发生异常", e);
return R.fail("系统繁忙,请稍后重试。");
} finally {
redisLock.unlock(packetLockName);
redisLock.unlock(lockName);
}
}
/**
* 尝试从推广CDKEY表兑换
* 仅主播账号创建的CDKEY,且主播下级普通用户可领取
*/
private R tryRedeemPromoCdkey(String code, TtUser ttUser) {
if (StringUtils.isBlank(code)) return R.fail("口令错误。");
java.util.Map<String, Object> cdkey = promoCdkeyUserMapper.findActiveCdkey(code);
if (cdkey == null) {
log.info("CDKEY兑换失败: userId={}, code={}, 原因=查不到进行中的CDKEY", ttUser.getUserId(), code);
return R.fail("口令错误,没有合法的红包。");
}
Long cdkeyId = ((Number) cdkey.get("id")).longValue();
String cdkeyCode = (String) cdkey.get("cdkey_code");
BigDecimal amountMin = cdkey.get("amount_min") != null ? new BigDecimal(cdkey.get("amount_min").toString()) : BigDecimal.ZERO;
BigDecimal amountMax = cdkey.get("amount_max") != null ? new BigDecimal(cdkey.get("amount_max").toString()) : BigDecimal.ZERO;
Integer usableCount = cdkey.get("usable_count") != null ? ((Number) cdkey.get("usable_count")).intValue() : 0;
Long creatorId = ((Number) cdkey.get("creator_id")).longValue();
log.info("CDKEY兑换开始: userId={}, cdkeyId={}, cdkeyCode={}, usableCount={}, creatorId={}, userType={}, parentId={}",
ttUser.getUserId(), cdkeyId, cdkeyCode, usableCount, creatorId, ttUser.getUserType(), ttUser.getParentId());
// 检查可用次数
if (usableCount <= 0) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=可用次数为{}", ttUser.getUserId(), cdkeyId, usableCount);
return R.fail("该CDKEY已兑完。");
}
// 检查开启时间
Object openingTimeObj = cdkey.get("opening_time");
if (openingTimeObj != null) {
java.time.LocalDateTime openingLdt = toLocalDateTime(openingTimeObj);
if (openingLdt != null && java.time.LocalDateTime.now().isBefore(openingLdt)) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=开启时间未到, openingTime={}, now={}",
ttUser.getUserId(), cdkeyId, openingLdt, java.time.LocalDateTime.now());
return R.fail("CDKEY尚未开启,敬请期待。");
}
}
// 检查有效期
Object validityObj = cdkey.get("validity");
if (validityObj != null) {
java.time.LocalDateTime validityLdt = toLocalDateTime(validityObj);
if (validityLdt != null && java.time.LocalDateTime.now().isAfter(validityLdt)) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=已过期, validity={}, now={}",
ttUser.getUserId(), cdkeyId, validityLdt, java.time.LocalDateTime.now());
return R.fail("该CDKEY已过期。");
}
}
// 检查用户类型:仅普通用户可兑换
if (!UserType.COMMON_USER.getCode().equals(ttUser.getUserType())) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=用户类型不合法, userType={}",
ttUser.getUserId(), cdkeyId, ttUser.getUserType());
return R.fail("仅普通用户可兑换该CDKEY。");
}
// 检查是否为创建者下级
if (ttUser.getParentId() == null || ttUser.getParentId() != creatorId.intValue()) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=非创建者下级, parentId={}, creatorId={}",
ttUser.getUserId(), cdkeyId, ttUser.getParentId(), creatorId);
return R.fail("该CDKEY仅限主播下级用户领取。");
}
// 锁前快速检查(减少锁竞争,非安全校验)
int alreadyRedeemed = promoCdkeyUserMapper.countUserRedeem(cdkeyId, ttUser.getUserId().longValue());
if (alreadyRedeemed > 0) {
log.info("CDKEY兑换失败: userId={}, cdkeyId={}, 原因=重复兑换", ttUser.getUserId(), cdkeyId);
return R.fail("您已兑换过该CDKEY。");
}
// 加锁兑换(锁粒度: cdkeyId + userId,防止同一用户并发兑换同一CDKEY)
String lockName = "PROMO_CDKEY_REDEEM_LOCK:" + cdkeyId + ":" + ttUser.getUserId();
boolean lock = false;
for (int i = 0; i < 5; i++) {
lock = redisLock.tryLock(lockName, 3L, 7L, TimeUnit.SECONDS);
if (lock) break;
}
if (!lock) return R.fail("服务器繁忙,请稍后重试。");
try {
// 锁内二次检查(防TOCTOU竞态条件)
if (promoCdkeyUserMapper.countUserRedeem(cdkeyId, ttUser.getUserId().longValue()) > 0) {
log.info("CDKEY兑换失败(锁内二次检查): userId={}, cdkeyId={}, 原因=重复兑换", ttUser.getUserId(), cdkeyId);
return R.fail("您已兑换过该CDKEY。");
}
int updated = promoCdkeyUserMapper.decrementUsableCount(cdkeyId);
if (updated <= 0) return R.fail("CDKEY已兑完。");
// 计算兑换金额
BigDecimal receiveAmount;
if (amountMin.compareTo(amountMax) >= 0) {
receiveAmount = amountMin;
} else {
double random = amountMin.doubleValue() + Math.random() * (amountMax.doubleValue() - amountMin.doubleValue());
receiveAmount = BigDecimal.valueOf(random).setScale(2, RoundingMode.HALF_UP);
}
// 记录兑换(捕获唯一约束异常作为最后防线)
Date now = new Date();
try {
promoCdkeyUserMapper.insertRedeemRecord(cdkeyId, cdkeyCode,
ttUser.getUserId().longValue(), ttUser.getUserName(), receiveAmount, now);
} catch (DuplicateKeyException dke) {
log.warn("CDKEY兑换唯一约束冲突(最后防线拦截): userId={}, cdkeyId={}", ttUser.getUserId(), cdkeyId);
return R.fail("您已兑换过该CDKEY。");
}
// 可用次数用完则更新状态
promoCdkeyUserMapper.updateStatusIfExhausted(cdkeyId);
// 增加用户金币余额
userService.updateOnlyUserAccount(ttUser.getUserId(), receiveAmount, TtAccountRecordSource.PROMO_CDKEY_REDEEM);
return R.ok(receiveAmount);
} catch (Exception e) {
log.error("CDKEY兑换异常", e);
return R.fail("系统繁忙,请稍后重试。");
} finally {
redisLock.unlock(lockName);
}
}
@Override
public R<QueryVipRewardResponse> vipRewardList(TtUser ttUser) {
// 今日游戏流水(所有游戏消费 source)
var todayAmountCf = AsyncManager.me().supply(() -> {
LocalDateTime beginTime = LocalDate.now().atStartOfDay();
var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.ge(TtUserBlendErcash::getCreateTime, beginTime)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list();
if (CollectionUtils.isEmpty(records)) {
return BigDecimal.ZERO;
}
return records.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
});
// 昨日游戏流水(所有游戏消费 source)
var yesterdayAmountCf = AsyncManager.me().supply(() -> {
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime beginTime = yesterday.atStartOfDay();
LocalDateTime endTime = yesterday.atTime(23, 59, 59);
var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.between(TtUserBlendErcash::getCreateTime, beginTime, endTime)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list();
if (CollectionUtils.isEmpty(records)) {
return BigDecimal.ZERO;
}
return records.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
});
// 已领奖励:昨日的 VIP 流水返利总额(实时发放模式下,昨日消费产生的返利在昨日已实时入账)
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
LocalDateTime yesterdayEnd = LocalDate.now().atStartOfDay().minusNanos(1);
var yesterdayRewardRecords = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode())
.between(TtUserBlendErcash::getCreateTime, yesterdayStart, yesterdayEnd)
.list();
BigDecimal yesterdayRewardTotal = CollectionUtils.isEmpty(yesterdayRewardRecords)
? BigDecimal.ZERO
: yesterdayRewardRecords.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
var todayAmount = AsyncManager.getMs(todayAmountCf, 1000);
var yesterdayAmount = AsyncManager.getMs(yesterdayAmountCf, 1000);
QueryVipRewardResponse response = new QueryVipRewardResponse();
if (todayAmount != null) {
response.setTodayAmount(MoneyUtil.toStr(todayAmount));
if (ttUser.getVipLevel() != null && ttUser.getVipLevel() > 0) {
TtVipLevel ttVipLevel = vipLevelService.getById(ttUser.getVipLevel());
if (ttVipLevel == null) {
log.warn("用户【{}】VIP等级不存在![{}]", ttUser.getUserId(), ttUser.getVipLevel());
response.setTodayReward("0.00");
} else {
// 使用 flowRebatePercent(若为 null 则回退到 commissions)
BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null
? ttVipLevel.getFlowRebatePercent()
: (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO);
BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4,
RoundingMode.HALF_UP)
.multiply(todayAmount).setScale(2, RoundingMode.HALF_UP);
response.setTodayReward(MoneyUtil.toStr(rebate));
}
} else {
response.setTodayReward("0.00");
}
} else {
response.setTodayReward("0.00");
response.setTodayAmount("0.00");
}
if (yesterdayAmount != null) {
response.setYesterdayAmount(MoneyUtil.toStr(yesterdayAmount));
} else {
response.setYesterdayAmount("0.00");
}
if (yesterdayRewardTotal.compareTo(BigDecimal.ZERO) > 0) {
response.setYesterdayReward(MoneyUtil.toStr(yesterdayRewardTotal));
} else {
response.setYesterdayReward("0.00");
}
// VIP 等级列表(全量返回)
List<TtVipLevel> rewardLevels = vipLevelService.list().stream()
.filter(lv -> lv != null && lv.getId() != null)
.sorted(java.util.Comparator.comparing(TtVipLevel::getId))
.collect(java.util.stream.Collectors.toList());
response.setVipLevels(rewardLevels);
// 填充当前用户今日转赠次数信息
Integer userVipLevelId = ttUser.getVipLevel() != null ? ttUser.getVipLevel() : 0;
int dailyLimit = 0;
if (userVipLevelId > 0) {
TtVipLevel currentVip = vipLevelService.getById(userVipLevelId);
if (currentVip != null && currentVip.getTransferCount() != null) {
dailyLimit = currentVip.getTransferCount();
}
}
TtUserTransferDaily dailyRecord = userTransferDailyService.getOrCreateToday(
ttUser.getUserId(), userVipLevelId, dailyLimit);
response.setTransferCount(dailyLimit);
int used = dailyRecord.getUsedCount() != null ? dailyRecord.getUsedCount() : 0;
int pending = dailyRecord.getPendingCount() != null ? dailyRecord.getPendingCount() : 0;
response.setAvailableTransferCount(Math.max(dailyLimit - used - pending, 0));
// 填充 VIP 经验进度信息
TtUserVipProgressVo progress = userService.userVipProgress(ttUser.getUserId());
if (progress != null) {
response.setCurrentVipLevel(progress.getVipLevel());
response.setCurrentExp(progress.getCurrentExp());
response.setNextLevelExp(progress.getNextLevelExp());
response.setNeedExp(progress.getNeedExp());
response.setMaxLevel(progress.getMaxLevel());
response.setProgressText(progress.getProgressText());
}
return R.ok(response);
}
@Override
public R<VipFlowRewardResponse> vipFlowReward(TtUser ttUser) {
// 今日游戏流水
var todayAmountCf = AsyncManager.me().supply(() -> {
LocalDateTime beginTime = LocalDate.now().atStartOfDay();
var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.ge(TtUserBlendErcash::getCreateTime, beginTime)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list();
if (CollectionUtils.isEmpty(records)) {
return BigDecimal.ZERO;
}
return records.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
});
// 昨日游戏流水
var yesterdayAmountCf = AsyncManager.me().supply(() -> {
LocalDate yd = LocalDate.now().minusDays(1);
LocalDateTime begin = yd.atStartOfDay();
LocalDateTime end = yd.atTime(23, 59, 59);
var records = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.between(TtUserBlendErcash::getCreateTime, begin, end)
.in(TtUserBlendErcash::getSource, TtAccountRecordSource.getGameBetCodes()).list();
if (CollectionUtils.isEmpty(records)) {
return BigDecimal.ZERO;
}
return records.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
});
// 已领奖励:截至昨日 23:59:59 累计领取的 VIP 流水返利总额
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
var yesterdayRewardRecords = new LambdaQueryChainWrapper<>(userBlendErcashMapper)
.eq(TtUserBlendErcash::getUserId, ttUser.getUserId())
.eq(TtUserBlendErcash::getSource, TtAccountRecordSource.VIP_CONSUME_AWARD.getCode())
.lt(TtUserBlendErcash::getCreateTime, todayStart)
.list();
BigDecimal yesterdayRewardTotal = CollectionUtils.isEmpty(yesterdayRewardRecords)
? BigDecimal.ZERO
: yesterdayRewardRecords.stream()
.map(TtUserBlendErcash::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add).abs();
var todayAmount = AsyncManager.getMs(todayAmountCf, 1000);
var yesterdayAmount = AsyncManager.getMs(yesterdayAmountCf, 1000);
VipFlowRewardResponse response = new VipFlowRewardResponse();
// 今日流水 & 预计奖励
if (todayAmount != null) {
response.setTodayAmount(MoneyUtil.toStr(todayAmount));
if (ttUser.getVipLevel() != null && ttUser.getVipLevel() > 0) {
TtVipLevel ttVipLevel = vipLevelService.getById(ttUser.getVipLevel());
if (ttVipLevel == null) {
response.setTodayReward("0.00");
} else {
BigDecimal percent = ttVipLevel.getFlowRebatePercent() != null
? ttVipLevel.getFlowRebatePercent()
: (ttVipLevel.getCommissions() != null ? ttVipLevel.getCommissions() : BigDecimal.ZERO);
BigDecimal rebate = percent.divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)
.multiply(todayAmount).setScale(2, RoundingMode.HALF_UP);
response.setTodayReward(MoneyUtil.toStr(rebate));
}
} else {
response.setTodayReward("0.00");
}
} else {
response.setTodayReward("0.00");
response.setTodayAmount("0.00");
}
// 昨日流水 & 已领奖励
response.setYesterdayAmount(yesterdayAmount != null ? MoneyUtil.toStr(yesterdayAmount) : "0.00");
response.setYesterdayReward(yesterdayRewardTotal.compareTo(BigDecimal.ZERO) > 0
? MoneyUtil.toStr(yesterdayRewardTotal) : "0.00");
// VIP 经验进度(先获取,用于构建每级进度文本)
TtUserVipProgressVo progress = userService.userVipProgress(ttUser.getUserId());
long currentExp = (progress != null && progress.getCurrentExp() != null) ? progress.getCurrentExp() : 0L;
// VIP 等级列表(精简 + 每级进度文本)
List<TtVipLevel> allLevels = vipLevelService.list().stream()
.filter(lv -> lv != null && lv.getId() != null)
.sorted(java.util.Comparator.comparing(TtVipLevel::getId))
.collect(java.util.stream.Collectors.toList());
List<VipFlowRewardResponse.VipLevelItem> levelItems = new java.util.ArrayList<>();
for (int i = 0; i < allLevels.size(); i++) {
TtVipLevel lv = allLevels.get(i);
long threshold = lv.getRechargeThreshold() != null
? lv.getRechargeThreshold().setScale(0, RoundingMode.CEILING).longValue() : 0L;
// 进度文本计算:所有等级都显示到下一级阈值的距离
String lvProgress;
if (i == allLevels.size() - 1) {
// 最后一级:显示满级状态
lvProgress = threshold + "/max";
} else if (threshold <= 0) {
lvProgress = currentExp + "/0";
} else {
// 显示当前经验/下一级阈值(即使当前等级已满)
TtVipLevel nextLv = allLevels.get(i + 1);
long nextThreshold = nextLv.getRechargeThreshold() != null
? nextLv.getRechargeThreshold().setScale(0, RoundingMode.CEILING).longValue() : 0L;
lvProgress = currentExp + "/" + nextThreshold;
}
levelItems.add(VipFlowRewardResponse.VipLevelItem.builder()
.name(lv.getName())
.rechargeThreshold(lv.getRechargeThreshold())
.flowRebatePercent(lv.getFlowRebatePercent() != null
? lv.getFlowRebatePercent()
: (lv.getCommissions() != null ? lv.getCommissions() : BigDecimal.ZERO))
.progressText(lvProgress)
.build());
}
response.setVipLevels(levelItems);
// 填充全局经验进度
if (progress != null) {
response.setCurrentVipLevel(progress.getVipLevel());
response.setCurrentExp(progress.getCurrentExp());
response.setNextLevelExp(progress.getNextLevelExp());
response.setNeedExp(progress.getNeedExp());
response.setMaxLevel(progress.getMaxLevel());
response.setProgressText(progress.getProgressText());
}
return R.ok(response);
}
/**
* 兼容 LocalDateTime / java.util.Date 两种类型的时间字段
*/
private java.time.LocalDateTime toLocalDateTime(Object obj) {
if (obj == null) return null;
if (obj instanceof java.time.LocalDateTime) {
return (java.time.LocalDateTime) obj;
}
if (obj instanceof java.util.Date) {
return ((java.util.Date) obj).toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDateTime();
}
return null;
}
}
@@ -0,0 +1,306 @@
package com.ruoyi.user.service.impl;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtPromotionUpdateMapper;
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.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.AjaxResult;
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.MessageUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.sys.TtPromotionUpdate;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtUserAvatar;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.framework.websocket.connector.SessionRegistry;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.common.service.ApiSmsService;
import com.ruoyi.user.service.ApiLoginService;
import com.ruoyi.admin.service.UserIdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ApiLoginServiceImpl implements ApiLoginService {
@Autowired
private ApiSmsService apiSmsService;
@Autowired
private TtUserMapper userMapper;
@Autowired
private SysLoginService sysLoginService;
@Autowired
private TokenService tokenService;
@Autowired
private RedisCache redisCache;
@Autowired
private TtUserService userService;
@Autowired
private TtUserAvatarService userAvatarService;
@Autowired
private SessionRegistry sessionRegistry;
@Autowired
private ISysConfigService configService;
@Autowired
private TtPromotionUpdateMapper ttPromotionUpdateMapper;
@Autowired
private UserIdGenerator userIdGenerator;
@Override
public Pair<String, AjaxResult> login(String username, String password) {
sysLoginService.loginPreCheck(username, password);
LoginUser loginUser = sysLoginService.createLoginUser("api_" + username, password);
// 顶号处理:本次登录前清除该账号在其它设备上的登录Token,避免旧设备继续可调用接口(如开箱)
replicatedLogin(loginUser.getUsername());
recordLoginInfo(loginUser.getUserId());
return Pair.of(tokenService.createToken(loginUser), null);
}
@Override
public AjaxResult verificationCodeLogin(String phoneNumber, String code) {
if (StringUtils.isEmpty(phoneNumber)) {
return AjaxResult.error("手机号不能为空");
}
if (!Validator.isMobile(phoneNumber)) {
return AjaxResult.error("手机号格式错误,请检查手机号是否输入正确!");
}
if (StringUtils.isEmpty(code)) {
return AjaxResult.error("验证码不能为空");
}
TtUser ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one();
if (StringUtils.isNull(ttUser)) {
return AjaxResult.error("该手机号未在本站注册!");
}
// 分支①:4位数字 → 原有短信验证码登录逻辑
if (NumberUtil.isNumber(code) && code.trim().length() == 4) {
String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiLogin_" + phoneNumber);
if (!"success".equals(validateCaptcha)) {
return AjaxResult.error(validateCaptcha);
}
}
// 分支②:非4位数字 → 视为密码,前提是已完成滑块验证
/*else {
String verifyKey = "captcha:verified:" + phoneNumber;
String verifiedCaptchaId = redisCache.getCacheObject(verifyKey);
if (StringUtils.isEmpty(verifiedCaptchaId)) {
return AjaxResult.error("请先完成滑块验证");
}
// 滑块验证通过,将 code 视为密码直接登录
Pair<String, AjaxResult> pair = this.login(ttUser.getUserName(), code.trim());
redisCache.deleteObject(verifyKey); // 使用后清除,防止重放
if (pair.getValue() != null) {
return pair.getValue();
}
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, pair.getKey());
return ajax;
}*/
// 短信验证通过后的原有登录逻辑
if (StringUtils.isEmpty(ttUser.getRemark()) || !ttUser.getRemark().contains(":")) {
return AjaxResult.error("账号密码信息异常,请联系客服");
}
String password = ttUser.getRemark().substring(ttUser.getRemark().indexOf(":") + 1);
Pair<String, AjaxResult> pair = this.login(ttUser.getUserName(), password);
if (pair.getValue() != null) {
return pair.getValue();
}
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, pair.getKey());
return ajax;
}
@Override
public AjaxResult verificationCodeLoginOrRegister(String phoneNumber, String code, String parentInvitationCode) {
if (StringUtils.isEmpty(phoneNumber)) {
return AjaxResult.error("手机号不能为空");
}
if (!Validator.isMobile(phoneNumber)) {
return AjaxResult.error("手机号格式错误,请检查手机号是否输入正确!");
}
if (StringUtils.isEmpty(code)) {
return AjaxResult.error("验证码不能为空");
}
if (!NumberUtil.isNumber(code) || code.trim().length() != 4) {
return AjaxResult.error("验证码错误");
}
String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiLogin_" + phoneNumber);
if (!"success".equals(validateCaptcha)) {
return AjaxResult.error(validateCaptcha);
}
TtUser ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one();
if (StringUtils.isNull(ttUser)) {
String result = register(phoneNumber, parentInvitationCode);
if (StringUtils.isNotEmpty(result)) {
return AjaxResult.error(result);
}
ttUser = new LambdaQueryChainWrapper<>(userMapper).eq(TtUser::getPhoneNumber, phoneNumber).one();
}
if (StringUtils.isEmpty(ttUser.getRemark()) || !ttUser.getRemark().contains(":")) {
return AjaxResult.error("账号密码信息异常,请联系客服");
}
String password = ttUser.getRemark().substring(ttUser.getRemark().indexOf(":") + 1);
Pair<String, AjaxResult> pair = this.login(ttUser.getUserName(), password);
if (pair.getValue() != null) {
return pair.getValue();
}
AjaxResult ajax = AjaxResult.success();
ajax.put(Constants.TOKEN, pair.getKey());
return ajax;
}
private String register(String phoneNumber, String parentInvitationCode) {
String nickName = RandomUtils.getRandomName(new Random().nextInt(2));
String password = RandomUtil.randomString(10);
if (StringUtils.isEmpty(nickName)) return "用户昵称不能为空";
if (StringUtils.isEmpty(phoneNumber)) return "手机号不能为空";
if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!";
if (StringUtils.isEmpty(password)) return "用户密码不能为空";
TtUser ttUser = TtUser.builder().build();
ttUser.setUserName(phoneNumber);
ttUser.setEmail(phoneNumber + "@qq.com");
ttUser.setPhoneNumber(phoneNumber);
ttUser.setAccountAmount(BigDecimal.ZERO);
ttUser.setAccountCredits(BigDecimal.ZERO);
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
return "密码长度必须在5到20个字符之间";
}
if (!userService.checkPhoneUnique(ttUser)) return "注册失败," + "手机号'" + phoneNumber + "'已被注册!";
if (!userService.checkUserNameUnique(ttUser))
return "注册失败," + "用户名'" + ttUser.getUserName() + "'已存在!";
ttUser.setNickName(nickName);
ttUser.setUserType(UserType.COMMON_USER.getCode());
ttUser.setCommissionRate(new BigDecimal("2"));
List<TtUserAvatar> userAvatarList = new LambdaQueryChainWrapper<>(userAvatarService.getBaseMapper()).eq(TtUserAvatar::getIsDefault, "1").list();
if (!userAvatarList.isEmpty()) {
ttUser.setAvatar(userAvatarList.get(0).getAvatar());
} else ttUser.setAvatar("");
ttUser.setPassword(SecurityUtils.encryptPassword(password));
String registerRedPacketStr = configService.selectConfigByKey("registerRedPacket");
BigDecimal registerRedPacket = new BigDecimal(registerRedPacketStr);
ttUser.setAccountAmount(registerRedPacket);
ttUser.setInvitationCode(userService.getInvitationCode().toLowerCase());
if (StringUtils.isNotEmpty(parentInvitationCode) && parentInvitationCode.trim().length() == 6) {
TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
.eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase())
.eq(TtUser::getDelFlag, "0").one();
if (StringUtils.isNull(parentUser)) {
return "上级邀请码填写错误!";
} else {
}
ttUser.setParentId(parentUser.getUserId());
}
ttUser.setIsRealCheck("0");
ttUser.setCreateBy("网站注册");
ttUser.setCreateTime(DateUtils.getNowDate());
ttUser.setRemark("明文密码:" + password);
var id = userIdGenerator.generateUserId();
ttUser.setUserId(id);
boolean regFlag = userService.save(ttUser);
if (!regFlag) return "注册失败,请联系系统管理人员";
// if (BigDecimal.ZERO.compareTo(registerRedPacket) < 0) {
// userService.insertUserAmountRecords(ttUser.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.REGIST_AWARD, registerRedPacket, ttUser.getAccountAmount());
// }
AsyncManager.me().execute(AsyncFactory
.recordLogininfor("api_" + ttUser.getUserName(), Constants.REGISTER, MessageUtils.message("user.register.success")));
// 保存推广更新记录(非核心数据,失败不影响注册主流程)
try {
TtPromotionUpdate build = TtPromotionUpdate.builder()
.employeeId(ttUser.getUserId())
.bossId(ttUser.getParentId())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
ttPromotionUpdateMapper.insert(build);
} catch (Exception e) {
log.warn("用户[{}]注册后保存推广更新记录失败: {}", ttUser.getUserId(), e.getMessage());
}
return "";
}
/**
* 检测用户是否在其它地方登录,如果存在则发送通知给当前用户,
* 并执行相应的清理工作,例如删除已登录用户的缓存信息
*/
private AjaxResult replicatedLogin(String username) {
Collection<String> redisKeys = redisCache.keys(CacheConstants.LOGIN_TOKEN_KEY + "*");
if (redisKeys == null || redisKeys.isEmpty()) {
return null;
}
List<LoginUser> loginUserList = redisKeys.stream().map(redisCache::<LoginUser>getCacheObject)
.filter(Objects::nonNull)
// api端用户 user 恒为 null(走 userData),以此区分,避免误踢后台/运营用户
.filter(loginUser -> loginUser.getUser() == null)
.filter(loginUser -> username.equals(loginUser.getUsername()))
.collect(Collectors.toList());
if (loginUserList.isEmpty()) {
return null;
}
// 遍历踢掉该账号在其它设备上的所有历史登录(可能残留多个Token),
// 确保任意端(手机/电脑)同一账号严格只保留最新一次登录的Token,其余全部失效。
for (LoginUser oldLoginUser : loginUserList) {
sessionRegistry.kickOut(oldLoginUser.getUserId(), "您的账号在别处登录,如果不是本人操作,请尽快修改密码!");
redisCache.deleteObject(CacheConstants.LOGIN_TOKEN_KEY + oldLoginUser.getToken());
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + username,
Constants.LOGOUT, "退出成功,账号已换机登录!"));
return null;
}
/**
* 记录登录信息
*/
public void recordLoginInfo(Long userId) {
// 改为同步执行,确保登录时必定更新 login_date(避免异步任务丢失或延迟)
try {
new LambdaUpdateChainWrapper<>(userMapper)
.eq(TtUser::getUserId, userId)
.set(TtUser::getLoginIp, IpUtils.getIpAddr())
.set(TtUser::getLoginDate, DateUtils.getNowDate())
.update();
} catch (Exception e) {
log.error("更新用户登录信息失败, userId={}", userId, e);
}
}
}
@@ -0,0 +1,59 @@
package com.ruoyi.user.service.impl;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.domain.other.TtMessageSend;
import com.ruoyi.admin.service.TtMessageSendService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.user.mapper.ApiMessageMapper;
import com.ruoyi.user.service.ApiMessageService;
import com.ruoyi.domain.vo.ApiMessageDataVO;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ApiMessageServiceImpl implements ApiMessageService {
private final ApiMessageMapper apiMessageMapper;
private final TtMessageSendService messageSendService;
public ApiMessageServiceImpl(ApiMessageMapper apiMessageMapper,
TtMessageSendService messageSendService) {
this.apiMessageMapper = apiMessageMapper;
this.messageSendService = messageSendService;
}
@Override
public List<ApiMessageDataVO> getMessageList(Long userId, Integer id) {
return apiMessageMapper.getMessageList(userId, id);
}
@Override
public ApiMessageDataVO view(Long userId, Integer id) {
TtMessageSend ttMessageSend = messageSendService.getById(id);
ttMessageSend.setStatus("1");
ttMessageSend.setReadingTime(DateUtils.getNowDate());
messageSendService.updateById(ttMessageSend);
List<ApiMessageDataVO> messageList = apiMessageMapper.getMessageList(userId, id);
if (messageList.isEmpty()) return null;
return messageList.get(0);
}
@Override
public String batchOperation(Long userId, List<Integer> ids, String status) {
List<TtMessageSend> list = new LambdaQueryChainWrapper<>(messageSendService.getBaseMapper())
.eq(TtMessageSend::getRecId, userId)
.in(TtMessageSend::getId, ids)
.list();
list = list.stream().peek(ttMessageSend -> {
ttMessageSend.setStatus(status);
if (StringUtils.isNull(ttMessageSend.getReadingTime())) {
ttMessageSend.setReadingTime(DateUtils.getNowDate());
}
}).collect(Collectors.toList());
messageSendService.updateBatchById(list, 1);
return "";
}
}
@@ -0,0 +1,72 @@
package com.ruoyi.user.service.impl;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.domain.other.TtNotice;
import com.ruoyi.user.mapper.ApiNoticeMapper;
import com.ruoyi.user.model.vo.ApiNoticeVO;
import com.ruoyi.user.service.ApiNoticeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
public class ApiNoticeServiceImpl implements ApiNoticeService {
@Autowired
private ApiNoticeMapper apiNoticeMapper;
@Override
public List<ApiNoticeVO> getNoticeList(Long userId) {
return apiNoticeMapper.getNoticeList(userId);
}
@Override
public List<ApiNoticeVO> getNoticeListByCategoryPrefix(Long userId, String categoryPrefix) {
return apiNoticeMapper.getNoticeListByCategoryPrefix(userId, categoryPrefix);
}
@Override
public ApiNoticeVO getNoticeByNoticeId(Long userId, Integer noticeId) {
// 读取详情后更新为已读状态
TtNotice ttNotice = new TtNotice();
ttNotice.setNoticeId(noticeId);
ttNotice.setRead("1");
editNotice(ttNotice);
return apiNoticeMapper.getNoticeByNoticeId(userId, noticeId);
}
@Override
public int countUnreadNotice(Long userId) {
return apiNoticeMapper.countUnreadNotice(userId);
}
@Override
public int addNotice(TtNotice ttNotice) {
ttNotice.setCreateTime(new Date());
return apiNoticeMapper.addNotice(ttNotice);
}
@Override
public int editNotice(TtNotice ttNotice) {
return apiNoticeMapper.editNotice(ttNotice);
}
@Override
public int removeNoticeByNoticeId(Integer noticeId) {
return apiNoticeMapper.removeNoticeByNoticeId(noticeId);
}
@Override
public int markNoticesAsReadByCategory(String categoryPrefix) {
Long userId = SecurityUtils.getUserId();
if (userId == null) {
throw new RuntimeException("用户未登录");
}
if (categoryPrefix == null || categoryPrefix.isEmpty()) {
throw new RuntimeException("分类前缀不能为空");
}
return apiNoticeMapper.markNoticesAsReadByCategory(userId, categoryPrefix);
}
}
@@ -0,0 +1,457 @@
package com.ruoyi.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.mapper.TtRechargeRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.dto.userRecord.ProfileRecordsRequest;
import com.ruoyi.domain.entity.BoxTransferRecord;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.delivery.TtDeliveryRecord;
import com.ruoyi.domain.other.TtRechargeRecord;
import com.ruoyi.domain.vo.userRecord.ProfileRecordItemVO;
import com.ruoyi.domain.vo.userRecord.ProfileRecordsResponse;
import com.ruoyi.user.mapper.ApiBoxTransferRecordMapper;
import com.ruoyi.user.service.ApiProfileRecordsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 个人资产详情 - 我的记录 聚合查询实现。
* 一个接口承载 4 个 Tab:余额/充值/提货/赠送,按 tab 分发到对应 handler。
*/
@Slf4j
@Service
public class ApiProfileRecordsServiceImpl implements ApiProfileRecordsService {
/** 余额-充值 subFilter=2 覆盖的 source 集合 */
private static final Set<Integer> BALANCE_RECHARGE_SOURCES = Set.of(
1
);
/** 余额-奖励 subFilter=5 覆盖的 source 集合 */
private static final Set<Integer> BALANCE_REWARD_SOURCES = Set.of(
3, 5, 7, 9, 11, 13, 14, 17, 18,
19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
37, 39, 41, 43, 44, 46, 47, 48, 49, 52, 56,
// 61=玫瑰女郎奖励, 62=招商周结佣金, 63=流水抽奖奖励
61, 62, 63
);
/** 余额-消费 subFilter=1 覆盖的 source 集合(59=主播Roll房奖池扣款) */
private static final Set<Integer> BALANCE_CONSUMPTION_SOURCES = Set.of(
2, 4, 6, 8, 12, 14, 33, 34, 38, 40, 42, 45, 50, 59,
// 53=主播创建CDKEY消费, 57=后台生成主播CDK红包扣款, 60=玫瑰女郎消费
53, 57, 60
);
/** 余额-结算 subFilter=4 覆盖的 source 集合 */
private static final Set<Integer> BALANCE_SETTLEMENT_SOURCES = Set.of(
10, 16,
// 54=主播CDKEY结束退还, 55=卡商销毁卡密退还, 64=疯狂转盘注单退款(兜底结算专用)
54, 55, 64
);
/** source -> 类型文案 */
private static final Map<Integer, String> SOURCE_MSG_MAP;
/** 分页默认每页条数 */
private static final int DEFAULT_PAGE_SIZE = 20;
/** 分页每页最大条数 */
private static final int MAX_PAGE_SIZE = 100;
static {
Map<Integer, String> m = new HashMap<>();
for (TtAccountRecordSource s : TtAccountRecordSource.values()) {
m.put(s.getCode(), s.getMsg());
}
SOURCE_MSG_MAP = Collections.unmodifiableMap(m);
}
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private TtRechargeRecordMapper ttRechargeRecordMapper;
@Autowired
private com.ruoyi.admin.mapper.TtDeliveryRecordMapper ttDeliveryRecordMapper;
@Autowired
private TtOrnamentMapper ttOrnamentMapper;
@Autowired
private TtBoxRecordsMapper ttBoxRecordsMapper;
@Autowired
private ApiBoxTransferRecordMapper boxTransferRecordMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Override
public ProfileRecordsResponse queryRecords(Integer userId, ProfileRecordsRequest request) {
Timestamp[] range = resolveTimeRange(request.getTimeRange());
Timestamp beginTime = range[0];
Timestamp endTime = range[1];
switch (request.getTab()) {
case 1:
return queryBalanceRecords(userId, beginTime, endTime, request);
case 2:
return queryRechargeRecords(userId, beginTime, endTime, request);
case 3:
return queryDeliveryRecords(userId, beginTime, endTime, request);
case 4:
return queryTransferRecords(userId, beginTime, endTime, request);
default:
return emptyResponse(request);
}
}
/* ===================== Tab 1 余额记录 ===================== */
private ProfileRecordsResponse queryBalanceRecords(Integer userId, Timestamp beginTime,
Timestamp endTime, ProfileRecordsRequest request) {
// 不传或 0 视为全部;1=消费 2=充值 4=结算 5=奖励,均按 source 集合过滤
Integer subFilter = request.getSubFilter() == null ? 0 : request.getSubFilter();
LambdaQueryWrapper<TtUserBlendErcash> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtUserBlendErcash::getUserId, userId)
.between(TtUserBlendErcash::getCreateTime, beginTime, endTime)
// 过滤金币与积分双零的脏数据;某些结算收入(如分解饰品、CDK)仅写 credits 列,应该也要展示
.and(w -> w.ne(TtUserBlendErcash::getAmount, BigDecimal.ZERO)
.or().ne(TtUserBlendErcash::getCredits, BigDecimal.ZERO))
// 排除 Roll房未发放退回主播的纯积分记录(source=56 且金币额为0):主播余额记录只展示金币退回,不展示积分退回
.and(w -> w.ne(TtUserBlendErcash::getSource, TtAccountRecordSource.ROLL_UNISSUED_REFUND.getCode())
.or().ne(TtUserBlendErcash::getAmount, BigDecimal.ZERO));
if (subFilter == 1) { // 消费
wrapper.in(TtUserBlendErcash::getSource, BALANCE_CONSUMPTION_SOURCES);
} else if (subFilter == 2) { // 充值
wrapper.in(TtUserBlendErcash::getSource, BALANCE_RECHARGE_SOURCES);
} else if (subFilter == 4) { // 结算
wrapper.in(TtUserBlendErcash::getSource, BALANCE_SETTLEMENT_SOURCES);
} else if (subFilter == 5) { // 奖励
wrapper.in(TtUserBlendErcash::getSource, BALANCE_REWARD_SOURCES);
}
// subFilter==0/null/其他值 → 全部,不附加 source 过滤
wrapper.orderByDesc(TtUserBlendErcash::getCreateTime);
IPage<TtUserBlendErcash> page = ttUserBlendErcashMapper.selectPage(buildPage(request), wrapper);
List<TtUserBlendErcash> records = page.getRecords();
List<ProfileRecordItemVO> items = records.stream().map(e -> {
ProfileRecordItemVO vo = new ProfileRecordItemVO();
vo.setRecordTime(e.getCreateTime());
vo.setTypeCode(e.getSource());
vo.setType(SOURCE_MSG_MAP.getOrDefault(e.getSource(), "其他"));
// DB 中 amount/credits 已带正确符号(收入正、支出负),直接使用无需 abs/negate
BigDecimal amount = e.getAmount();
if (amount == null || amount.compareTo(BigDecimal.ZERO) == 0) {
amount = e.getCredits() == null ? BigDecimal.ZERO : e.getCredits();
}
vo.setAmount(amount);
vo.setBalance(e.getFinalAmount());
return vo;
}).collect(Collectors.toList());
return buildResponse(items, page.getTotal(), request, null);
}
/* ===================== Tab 2 充值记录 ===================== */
private ProfileRecordsResponse queryRechargeRecords(Integer userId, Timestamp beginTime,
Timestamp endTime, ProfileRecordsRequest request) {
LambdaQueryWrapper<TtRechargeRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtRechargeRecord::getUserId, userId)
.eq(TtRechargeRecord::getStatus, "0") // 仅查询充值成功记录
// 白号空白卡密充值记录(is_blank_card=1)需在 cocos 展示,故此处不再过滤;后台按 is_blank_card 过滤隐藏
.between(TtRechargeRecord::getCreateTime, beginTime, endTime)
.orderByDesc(TtRechargeRecord::getCreateTime);
IPage<TtRechargeRecord> page = ttRechargeRecordMapper.selectPage(buildPage(request), wrapper);
List<TtRechargeRecord> records = page.getRecords();
List<ProfileRecordItemVO> items = records.stream().map(e -> {
ProfileRecordItemVO vo = new ProfileRecordItemVO();
vo.setRecordTime(e.getCreateTime());
vo.setOrderNo(e.getOrderId());
vo.setAmount(e.getAmountActuallyPaid());
return vo;
}).collect(Collectors.toList());
// 统计:当前时间段内所有成功充值的总金额(不分页)
BigDecimal totalAmount = sumRechargeAmount(userId, beginTime, endTime);
ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO();
summary.setTotalAmount(totalAmount);
return buildResponse(items, page.getTotal(), request, summary);
}
private BigDecimal sumRechargeAmount(Integer userId, Timestamp beginTime, Timestamp endTime) {
QueryWrapper<TtRechargeRecord> qw = new QueryWrapper<>();
qw.select("IFNULL(SUM(amount_actually_paid), 0) AS total")
.eq("user_id", userId)
.eq("status", "0")
// 白号空白卡密充值需计入 cocos 充值总额,故此处不再过滤 is_blank_card
.between("create_time", beginTime, endTime);
Object total = ttRechargeRecordMapper.selectMaps(qw).stream()
.findFirst()
.map(m -> m.get("total"))
.orElse(null);
return total == null ? BigDecimal.ZERO : new BigDecimal(total.toString());
}
/* ===================== Tab 3 提货记录 ===================== */
private ProfileRecordsResponse queryDeliveryRecords(Integer userId, Timestamp beginTime,
Timestamp endTime, ProfileRecordsRequest request) {
LambdaQueryWrapper<TtDeliveryRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtDeliveryRecord::getUserId, userId)
.between(TtDeliveryRecord::getCreateTime, beginTime, endTime)
.orderByDesc(TtDeliveryRecord::getCreateTime);
IPage<TtDeliveryRecord> page = ttDeliveryRecordMapper.selectPage(buildPage(request), wrapper);
List<TtDeliveryRecord> records = page.getRecords();
// 批量查饰品信息(全名 + 图片)
Set<Long> ornamentIds = records.stream()
.map(TtDeliveryRecord::getOrnamentId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
Map<Long, TtOrnament> ornamentMap = ornamentIds.isEmpty()
? Collections.emptyMap()
: new LambdaQueryChainWrapper<>(ttOrnamentMapper)
.in(TtOrnament::getId, ornamentIds)
.list().stream()
.collect(Collectors.toMap(TtOrnament::getId, o -> o, (a, b) -> a));
// 批量查 box_records 获取品质图片
Set<Long> boxRecordIds = records.stream()
.map(TtDeliveryRecord::getBoxRecordsId)
.filter(Objects::nonNull)
.collect(Collectors.toCollection(HashSet::new));
Map<Long, String> levelImgMap = boxRecordIds.isEmpty()
? Collections.emptyMap()
: ttBoxRecordsMapper.selectBatchIds(boxRecordIds).stream()
.filter(br -> br.getOrnamentLevelImg() != null)
.collect(Collectors.toMap(TtBoxRecords::getId, TtBoxRecords::getOrnamentLevelImg, (a, b) -> a));
List<ProfileRecordItemVO> items = records.stream().map(e -> {
ProfileRecordItemVO vo = new ProfileRecordItemVO();
vo.setRecordTime(e.getCreateTime());
vo.setAmount(e.getOrnamentsPrice());
// 饰品全名 + 图片
TtOrnament ornament = ornamentMap.get(e.getOrnamentId());
if (ornament != null) {
vo.setOrnamentName(ornament.getName());
vo.setOrnamentImg(ornament.getImageUrl());
}
// 品质图片
if (e.getBoxRecordsId() != null) {
vo.setOrnamentLevelImg(levelImgMap.get(e.getBoxRecordsId()));
}
return vo;
}).collect(Collectors.toList());
// 统计:当前时间段内所有提货金额总和(不分页)
BigDecimal totalAmount = sumDeliveryAmount(userId, beginTime, endTime);
ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO();
summary.setTotalAmount(totalAmount);
return buildResponse(items, page.getTotal(), request, summary);
}
private BigDecimal sumDeliveryAmount(Integer userId, Timestamp beginTime, Timestamp endTime) {
QueryWrapper<TtDeliveryRecord> qw = new QueryWrapper<>();
qw.select("IFNULL(SUM(ornaments_price), 0) AS total")
.eq("user_id", userId)
.between("create_time", beginTime, endTime);
Object total = ttDeliveryRecordMapper.selectMaps(qw).stream()
.findFirst()
.map(m -> m.get("total"))
.orElse(null);
return total == null ? BigDecimal.ZERO : new BigDecimal(total.toString());
}
/* ===================== Tab 4 赠送记录 ===================== */
private ProfileRecordsResponse queryTransferRecords(Integer userId, Timestamp beginTime,
Timestamp endTime, ProfileRecordsRequest request) {
Integer subFilter = request.getSubFilter() == null ? 0 : request.getSubFilter();
long uid = userId.longValue();
IPage<BoxTransferRecord> page = boxTransferRecordMapper.selectPage(
buildPage(request), buildTransferWrapper(userId, subFilter, beginTime, endTime));
List<BoxTransferRecord> records = page.getRecords();
// 批量收集对方 userId,一次性查询昵称,避免 N+1
Set<Integer> counterpartyIds = new HashSet<>();
for (BoxTransferRecord e : records) {
Long counterparty = Objects.equals(e.getSrcUid(), uid) ? e.getDstUid() : e.getSrcUid();
if (counterparty != null) {
counterpartyIds.add(counterparty.intValue());
}
}
Map<Integer, String> nickNameMap = counterpartyIds.isEmpty()
? Collections.emptyMap()
: ttUserMapper.selectBatchIds(counterpartyIds).stream()
.collect(Collectors.toMap(TtUser::getUserId, TtUser::getNickName, (a, b) -> a));
List<ProfileRecordItemVO> items = records.stream().map(e -> {
ProfileRecordItemVO vo = new ProfileRecordItemVO();
// BoxTransferRecord.createTime 为 LocalDateTime,转 Date 保持 VO 统一
if (e.getCreateTime() != null) {
vo.setRecordTime(Date.from(e.getCreateTime().atZone(ZoneId.systemDefault()).toInstant()));
}
vo.setOrnamentName(e.getOrnamentName());
vo.setOrnamentImg(e.getImageUrl());
vo.setOrnamentLevelImg(e.getOrnamentLevelImg());
vo.setAmount(e.getOrnamentsPrice());
int direction = Objects.equals(e.getSrcUid(), uid) ? 1 : 2;
vo.setDirection(direction);
vo.setTypeCode(direction);
vo.setType(direction == 1 ? "赠送" : "受赠");
// 对方昵称及用户ID:赠送时为受赠人,受赠时为赠送人
Long counterparty = direction == 1 ? e.getDstUid() : e.getSrcUid();
if (counterparty != null) {
vo.setNickName(nickNameMap.get(counterparty.intValue()));
}
vo.setCounterpartyId(counterparty);
// 转赠记录三态:0-审核中,1-成功,2-失败
Integer status = e.getStatus();
if (status == null) {
vo.setStatus("审核中");
} else {
switch (status) {
case 0: vo.setStatus("审核中"); break;
case 1: vo.setStatus("已完成"); break; // 审核通过标记为"已完成"
case 2: vo.setStatus("已驳回"); break;
default: vo.setStatus("未知状态"); break;
}
}
return vo;
}).collect(Collectors.toList());
ProfileRecordsResponse.SummaryVO summary = new ProfileRecordsResponse.SummaryVO();
// completedCount = status=1(成功) + status=2(失败),pendingCount = status 为 null 或 0(审核中);统计整个时间段,不随分页变化
long completedCount = boxTransferRecordMapper.selectCount(
buildTransferWrapper(userId, subFilter, beginTime, endTime)
.in(BoxTransferRecord::getStatus, 1, 2));
long pendingCount = boxTransferRecordMapper.selectCount(
buildTransferWrapper(userId, subFilter, beginTime, endTime)
.and(w -> w.isNull(BoxTransferRecord::getStatus).or().eq(BoxTransferRecord::getStatus, 0)));
summary.setCompletedCount(completedCount);
summary.setPendingCount(pendingCount);
return buildResponse(items, page.getTotal(), request, summary);
}
/**
* 构建赠送记录查询条件(分页查询与统计计数共用)。
* 白号假转赠:作为受赠方(dstUid)时不展示来自白号的转赠记录;作为赠送方(srcUid)不受影响。
*/
private LambdaQueryWrapper<BoxTransferRecord> buildTransferWrapper(Integer userId, Integer subFilter,
Timestamp beginTime, Timestamp endTime) {
long uid = userId.longValue();
LambdaQueryWrapper<BoxTransferRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.between(BoxTransferRecord::getCreateTime, beginTime.toLocalDateTime(), endTime.toLocalDateTime());
// 假号(白号 is_blank_account=1 / 机器人 user_type='03')来源的转赠记录不展示给接收方(假转赠)
final String BLANK_SRC_EXCLUDE =
"SELECT user_id FROM tt_user WHERE is_blank_account = 1 OR user_type = '03'";
if (subFilter == 1) {
wrapper.eq(BoxTransferRecord::getSrcUid, uid);
} else if (subFilter == 2) {
wrapper.eq(BoxTransferRecord::getDstUid, uid)
.notInSql(BoxTransferRecord::getSrcUid, BLANK_SRC_EXCLUDE);
} else {
wrapper.and(w -> w.eq(BoxTransferRecord::getSrcUid, uid)
.or(x -> x.eq(BoxTransferRecord::getDstUid, uid)
.notInSql(BoxTransferRecord::getSrcUid, BLANK_SRC_EXCLUDE)));
}
return wrapper.orderByDesc(BoxTransferRecord::getCreateTime);
}
/* ===================== 工具方法 ===================== */
/**
* 解析时间区间。
* 1=今日(0:00 ~ 当前)
* 2=昨日(昨日0:00 ~ 昨日23:59:59)
* 3=近7日(7天前0:00 ~ 当前);不传 timeRange 也走这里,即接口最多只返回近 7 天数据
*/
private Timestamp[] resolveTimeRange(Integer timeRange) {
// 不传或非法值统一按 近7日 处理,保证接口任何情况下都不会返回 7 天以前的数据
int range = (timeRange == null) ? 3 : timeRange;
LocalDate today = LocalDate.now();
LocalDateTime begin;
LocalDateTime end;
if (range == 1) {
begin = today.atStartOfDay();
end = LocalDateTime.now();
} else if (range == 2) {
LocalDate yesterday = today.minusDays(1);
begin = yesterday.atStartOfDay();
end = yesterday.atTime(LocalTime.MAX);
} else {
begin = today.minusDays(6).atStartOfDay();
end = LocalDateTime.now();
}
return new Timestamp[]{Timestamp.valueOf(begin), Timestamp.valueOf(end)};
}
/** 构建分页对象:pageNum 默认 1,pageSize 默认 20 且上限 100 */
private <T> Page<T> buildPage(ProfileRecordsRequest request) {
int pageNum = (request.getPageNum() == null || request.getPageNum() < 1) ? 1 : request.getPageNum();
int pageSize = (request.getPageSize() == null || request.getPageSize() < 1)
? DEFAULT_PAGE_SIZE : Math.min(request.getPageSize(), MAX_PAGE_SIZE);
return new Page<>(pageNum, pageSize);
}
private ProfileRecordsResponse buildResponse(List<ProfileRecordItemVO> items,
Long total,
ProfileRecordsRequest request,
ProfileRecordsResponse.SummaryVO summary) {
ProfileRecordsResponse response = new ProfileRecordsResponse();
response.setList(items == null ? new ArrayList<>() : items);
response.setTotal(total == null ? 0L : total);
response.setSummary(summary);
return response;
}
private ProfileRecordsResponse emptyResponse(ProfileRecordsRequest request) {
ProfileRecordsResponse response = new ProfileRecordsResponse();
response.setList(new ArrayList<>());
response.setTotal(0L);
return response;
}
}
@@ -0,0 +1,65 @@
package com.ruoyi.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.mapper.TtPromotionRecordMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.domain.entity.TtPromotionRecord;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.vo.promotion.TtPromotionRecordVo;
import com.ruoyi.user.service.ApiPromotionRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class ApiPromotionRecordServiceImpl implements ApiPromotionRecordService {
@Autowired
private TtPromotionRecordMapper ttPromotionRecordMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Override
public List<TtPromotionRecordVo> getPromotionRecord(Integer userId) {
LambdaQueryWrapper<TtPromotionRecord> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtPromotionRecord::getUserId, userId);
wrapper.orderByDesc(TtPromotionRecord::getId);
List<TtPromotionRecord> list = ttPromotionRecordMapper.selectList(wrapper);
if (CollectionUtils.isEmpty(list)) {
return Collections.emptyList();
}
List<TtPromotionRecordVo> vos = new ArrayList<>();
List<Integer> userIds = new ArrayList<>();
for (TtPromotionRecord record : list) {
userIds.add(record.getSubordinateUserId());
TtPromotionRecordVo vo = new TtPromotionRecordVo();
BeanUtils.copyProperties(record, vo);
if (record.getRebate() == null || record.getRebate().compareTo(new BigDecimal(0)) <= 0) {
vo.setCommissions(new BigDecimal(0));
} else if (record.getRechargePrice().compareTo(new BigDecimal(0)) > 0) {
vo.setCommissions(record.getRebate().multiply(BigDecimal.valueOf(100))
.divide(record.getRechargePrice(), 1, RoundingMode.HALF_UP));
}
vos.add(vo);
}
List<TtUser> ttUsers = ttUserMapper.selectBatchIds(userIds);
Map<Integer, String> ttUserMap = ttUsers.stream().filter(Objects::nonNull).collect(Collectors.toMap(
TtUser::getUserId, // 使用TtUser的userId作为Map的键
TtUser::getNickName // 使用TtUser的username作为Map的值
));
for (TtPromotionRecordVo vo : vos) {
vo.setSubordinateUserName(ttUserMap.getOrDefault(vo.getSubordinateUserId(), ""));
}
return vos;
}
}
@@ -0,0 +1,137 @@
package com.ruoyi.user.service.impl;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.NumberUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtPromotionUpdateMapper;
import com.ruoyi.admin.service.TtUserAvatarService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.utils.*;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.sys.TtPromotionUpdate;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiRegisterBody;
import com.ruoyi.domain.other.TtUserAvatar;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.baidu.service.BaiduService;
import com.ruoyi.thirdparty.common.service.ApiSmsService;
import com.ruoyi.user.service.ApiRegisterService;
import com.ruoyi.admin.service.UserIdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.List;
@Service
@Slf4j
public class ApiRegisterServiceImpl implements ApiRegisterService {
@Autowired
private TtUserAvatarService userAvatarService;
@Autowired
private TtUserService userService;
@Autowired
private ISysConfigService configService;
@Autowired
private ApiSmsService apiSmsService;
@Autowired
private TtPromotionUpdateMapper ttPromotionUpdateMapper;
@Autowired
private BaiduService baiduService;
@Autowired
private UserIdGenerator userIdGenerator;
@Override
public String register(ApiRegisterBody registerBody) {
String nickName = registerBody.getNickName(), phoneNumber = registerBody.getPhoneNumber(),
password = registerBody.getPassword(), parentInvitationCode = registerBody.getParentInvitationCode(),
code = registerBody.getCode();
if (StringUtils.isEmpty(nickName) || nickName.length() > UserConstants.NICKNAME_MAX_LENGTH) {
return "昵称必须在1到7个字符之间";
}
if (StringUtils.isEmpty(phoneNumber)) return "手机号不能为空";
if (!Validator.isMobile(phoneNumber)) return "手机号格式错误,请检查手机号是否输入正确!";
if (StringUtils.isEmpty(password)) return "用户密码不能为空";
if (StringUtils.isEmpty(code)) return "验证码不能为空";
//// TODO: 上线前恢复 → if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误";
//// 临时降级:验证码改为与密码一致即可通过
if (!code.trim().equals(password)) return "验证码错误,请输入与密码相同的内容";
//if (!NumberUtil.isNumber(code) || code.trim().length() != 4) return "验证码错误";
TtUser ttUser = TtUser.builder().build();
ttUser.setUserName(phoneNumber);
ttUser.setEmail(phoneNumber + "@qq.com");
ttUser.setPhoneNumber(phoneNumber);
ttUser.setAccountAmount(BigDecimal.ZERO);
ttUser.setAccountCredits(BigDecimal.ZERO);
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
return "密码长度必须在5到20个字符之间";
}
if (!userService.checkPhoneUnique(ttUser)) return "注册失败," + "手机号'" + phoneNumber + "'已被注册!";
if (!userService.checkUserNameUnique(ttUser))
return "注册失败," + "用户名'" + ttUser.getUserName() + "'已存在!";
ttUser.setNickName(nickName);
ttUser.setUserType("02");
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(password));
String registerRedPacketStr = configService.selectConfigByKey("registerRedPacket");
BigDecimal registerRedPacket = new BigDecimal(registerRedPacketStr);
ttUser.setAccountAmount(registerRedPacket);
ttUser.setInvitationCode(userService.getInvitationCode().toLowerCase());
if (StringUtils.isNotEmpty(parentInvitationCode) && parentInvitationCode.trim().length() == 6) {
TtUser parentUser = new LambdaQueryChainWrapper<>(userService.getBaseMapper())
.eq(TtUser::getInvitationCode, parentInvitationCode.trim().toUpperCase())
.eq(TtUser::getDelFlag, "0").one();
if (StringUtils.isNull(parentUser)) {
return "上级邀请码填写错误!";
} else {
}
ttUser.setParentId(parentUser.getUserId());
}
ttUser.setIsRealCheck("0");
ttUser.setCreateBy("网站注册");
ttUser.setCreateTime(DateUtils.getNowDate());
ttUser.setRemark("明文密码:" + password);
// ===== 改后 =====
// TODO: 上线前恢复 →
// String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiRegister_" + phoneNumber);
// if (!"success".equals(validateCaptcha)) return validateCaptcha;
//String validateCaptcha = apiSmsService.validateCaptcha(code.trim(), "ApiRegister_" + phoneNumber);
//if (!"success".equals(validateCaptcha)) return validateCaptcha;
ttUser.setUserId(userIdGenerator.generateUserId());
boolean regFlag = userService.save(ttUser);
if (!regFlag) return "注册失败,请联系系统管理人员";
else {
if (BigDecimal.ZERO.compareTo(registerRedPacket) < 0) {
userService.insertUserAmountRecords(ttUser.getUserId(), TtAccountRecordType.INPUT, TtAccountRecordSource.REGIST_AWARD, registerRedPacket, ttUser.getAccountAmount());
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor("api_" + ttUser.getUserName(), Constants.REGISTER, MessageUtils.message("user.register.success")));
// 保存推广更新记录
TtPromotionUpdate build = TtPromotionUpdate.builder()
.employeeId(ttUser.getUserId())
.bossId(ttUser.getParentId())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
ttPromotionUpdateMapper.insert(build);
// baiduService.upload(ttUser.getUserId(), 49, registerBody.getBdVid());
}
return "";
}
}

Some files were not shown because too many files have changed in this diff Show More