Compare commits
4 Commits
fb0c0c34a9
...
a510a4afcb
| Author | SHA1 | Date | |
|---|---|---|---|
| a510a4afcb | |||
| c38f62c3c1 | |||
| be921e144f | |||
| 778cf4a0cb |
@@ -0,0 +1,150 @@
|
||||
package com.yolo.keyborad.listener;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.yolo.keyborad.model.entity.KeyboardThemeStyles;
|
||||
import com.yolo.keyborad.model.entity.KeyboardThemes;
|
||||
import com.yolo.keyborad.model.vo.themes.KeyboardThemeStylesRespVO;
|
||||
import com.yolo.keyborad.model.vo.themes.KeyboardThemesRespVO;
|
||||
import com.yolo.keyborad.service.KeyboardThemeStylesService;
|
||||
import com.yolo.keyborad.service.KeyboardThemesService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 主题缓存初始化器
|
||||
* 在应用启动时按风格将主题缓存到Redis
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ThemeCacheInitializer implements ApplicationRunner {
|
||||
|
||||
/**
|
||||
* 主题按风格分组的缓存key前缀
|
||||
*/
|
||||
private static final String THEME_STYLE_KEY = "theme:style:";
|
||||
|
||||
/**
|
||||
* 所有风格列表的缓存key
|
||||
*/
|
||||
private static final String THEME_STYLES_KEY = "theme:styles";
|
||||
|
||||
/**
|
||||
* 所有主题列表的缓存key(风格ID=9999表示全部)
|
||||
*/
|
||||
private static final String THEME_ALL_KEY = "theme:style:all";
|
||||
|
||||
/**
|
||||
* 缓存过期时间(天)
|
||||
*/
|
||||
private static final long CACHE_EXPIRE_DAYS = 1;
|
||||
|
||||
@Resource
|
||||
private KeyboardThemesService themesService;
|
||||
|
||||
@Resource
|
||||
private KeyboardThemeStylesService themeStylesService;
|
||||
|
||||
@Resource(name = "objectRedisTemplate")
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
try {
|
||||
log.info("开始缓存主题数据到Redis...");
|
||||
|
||||
// 1. 缓存所有风格列表
|
||||
cacheAllStyles();
|
||||
|
||||
// 2. 按风格分组缓存主题
|
||||
cacheThemesByStyle();
|
||||
|
||||
log.info("主题数据缓存完成");
|
||||
} catch (Exception e) {
|
||||
log.error("缓存主题数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存所有风格列表
|
||||
*/
|
||||
private void cacheAllStyles() {
|
||||
List<KeyboardThemeStyles> stylesList = themeStylesService.lambdaQuery()
|
||||
.eq(KeyboardThemeStyles::getDeleted, false)
|
||||
.list();
|
||||
|
||||
List<KeyboardThemeStylesRespVO> stylesVOList = BeanUtil.copyToList(stylesList, KeyboardThemeStylesRespVO.class);
|
||||
redisTemplate.opsForValue().set(THEME_STYLES_KEY, stylesVOList, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存 {} 种主题风格", stylesVOList.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按风格分组缓存主题
|
||||
*/
|
||||
private void cacheThemesByStyle() {
|
||||
// 查询所有有效主题
|
||||
List<KeyboardThemes> allThemes = themesService.lambdaQuery()
|
||||
.eq(KeyboardThemes::getDeleted, false)
|
||||
.eq(KeyboardThemes::getThemeStatus, true)
|
||||
.orderByAsc(KeyboardThemes::getSort)
|
||||
.list();
|
||||
|
||||
// 转换为VO(不设置购买状态,缓存的是公共数据)
|
||||
List<KeyboardThemesRespVO> allThemesVO = allThemes.stream()
|
||||
.map(theme -> BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 缓存所有主题(风格ID=all)
|
||||
redisTemplate.opsForValue().set(THEME_ALL_KEY, allThemesVO, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存所有主题,共 {} 个", allThemesVO.size());
|
||||
|
||||
// 按风格分组
|
||||
Map<Long, List<KeyboardThemesRespVO>> themesByStyle = allThemesVO.stream()
|
||||
.collect(Collectors.groupingBy(KeyboardThemesRespVO::getThemeStyle));
|
||||
|
||||
// 按风格缓存主题
|
||||
for (Map.Entry<Long, List<KeyboardThemesRespVO>> entry : themesByStyle.entrySet()) {
|
||||
Long styleId = entry.getKey();
|
||||
List<KeyboardThemesRespVO> themes = entry.getValue();
|
||||
String key = THEME_STYLE_KEY + styleId;
|
||||
redisTemplate.opsForValue().set(key, themes, CACHE_EXPIRE_DAYS, TimeUnit.DAYS);
|
||||
log.info("已缓存风格ID={} 的主题,共 {} 个", styleId, themes.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动刷新缓存(可通过接口调用)
|
||||
*/
|
||||
public void refreshCache() {
|
||||
log.info("手动刷新主题缓存...");
|
||||
clearCache();
|
||||
cacheAllStyles();
|
||||
cacheThemesByStyle();
|
||||
log.info("主题缓存刷新完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除主题相关缓存
|
||||
*/
|
||||
public void clearCache() {
|
||||
// 删除风格列表缓存
|
||||
redisTemplate.delete(THEME_STYLES_KEY);
|
||||
redisTemplate.delete(THEME_ALL_KEY);
|
||||
|
||||
// 删除所有风格下的主题缓存
|
||||
var keys = redisTemplate.keys(THEME_STYLE_KEY + "*");
|
||||
if (keys != null && !keys.isEmpty()) {
|
||||
redisTemplate.delete(keys);
|
||||
}
|
||||
log.info("已清除主题相关缓存");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.yolo.keyborad.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.Date;
|
||||
import lombok.Data;
|
||||
@@ -18,6 +15,7 @@ import lombok.Data;
|
||||
*/
|
||||
@Schema(description="用户生成的邀请码表,用于邀请新用户注册/安装并建立邀请关系")
|
||||
@Data
|
||||
@KeySequence("invite_codes_id_seq")
|
||||
@TableName(value = "keyboard_user_invite_codes")
|
||||
public class KeyboardUserInviteCodes {
|
||||
/**
|
||||
@@ -75,4 +73,25 @@ public class KeyboardUserInviteCodes {
|
||||
@TableField(value = "used_count")
|
||||
@Schema(description="邀请码已使用次数")
|
||||
private Integer usedCount;
|
||||
|
||||
/**
|
||||
* 邀请码类型:USER=普通用户邀请码,TENANT=租户邀请码
|
||||
*/
|
||||
@TableField(value = "invite_type")
|
||||
@Schema(description="邀请码类型:USER=普通用户邀请码,AGENT=租户邀请码")
|
||||
private String inviteType;
|
||||
|
||||
/**
|
||||
* 邀请码所属租户ID(当inviteType=AGENT时使用)
|
||||
*/
|
||||
@TableField(value = "owner_tenant_id")
|
||||
@Schema(description="邀请码所属租户ID(当inviteType=AGENT时使用)")
|
||||
private Long ownerTenantId;
|
||||
|
||||
/**
|
||||
* 邀请码所属租户用户ID(当inviteType=AGENT时使用)
|
||||
*/
|
||||
@TableField(value = "owner_system_user_id")
|
||||
@Schema(description="邀请码所属租户用户ID(当inviteType=AGENT时使用)")
|
||||
private Long ownerSystemUserId;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import lombok.Data;
|
||||
|
||||
/*
|
||||
* @author: ziin
|
||||
* @date: 2025/12/19 13:26
|
||||
* @date: 2025/12/29 13:58
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -48,6 +48,12 @@ public class KeyboardUserInvites {
|
||||
@Schema(description = "使用的邀请码ID")
|
||||
private Long inviteCodeId;
|
||||
|
||||
/**
|
||||
* 绑定时关联的点击Token(通过邀请链接自动绑定时使用)
|
||||
*/
|
||||
@TableField(value = "click_token")
|
||||
@Schema(description = "绑定时关联的点击Token(通过邀请链接自动绑定时使用)")
|
||||
private String clickToken;
|
||||
|
||||
/**
|
||||
* 绑定方式:1=手动填写邀请码,2=邀请链接自动绑定,3=其他方式
|
||||
@@ -76,4 +82,39 @@ public class KeyboardUserInvites {
|
||||
@TableField(value = "bind_user_agent")
|
||||
@Schema(description = "userAgent")
|
||||
private String bindUserAgent;
|
||||
|
||||
/**
|
||||
* 邀请码类型快照:USER=普通用户邀请,AGENT=代理邀请
|
||||
*/
|
||||
@TableField(value = "invite_type")
|
||||
@Schema(description = "邀请码类型快照:USER=普通用户邀请,AGENT=代理邀请")
|
||||
private String inviteType;
|
||||
|
||||
/**
|
||||
* 收益结算归属租户ID(代理结算用,绑定时固化)
|
||||
*/
|
||||
@TableField(value = "profit_tenant_id")
|
||||
@Schema(description = "收益结算归属租户ID(代理结算用,绑定时固化)")
|
||||
private Long profitTenantId;
|
||||
|
||||
/**
|
||||
* 收益归因员工ID(用于区分租户员工/渠道,绑定时固化)
|
||||
*/
|
||||
@TableField(value = "profit_employee_id")
|
||||
@Schema(description = "收益归因员工ID(用于区分租户员工/渠道,绑定时固化)")
|
||||
private Long profitEmployeeId;
|
||||
|
||||
/**
|
||||
* 邀请人所属租户ID快照(便于审计/对账,可选)
|
||||
*/
|
||||
@TableField(value = "inviter_tenant_id")
|
||||
@Schema(description = "邀请人所属租户ID快照(便于审计/对账,可选)")
|
||||
private Long inviterTenantId;
|
||||
|
||||
/**
|
||||
* 邀请码字符串快照(便于排查,可选)
|
||||
*/
|
||||
@TableField(value = "invite_code")
|
||||
@Schema(description = "邀请码字符串快照(便于排查,可选)")
|
||||
private String inviteCode;
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapp
|
||||
import com.yolo.keyborad.model.entity.KeyboardThemePurchase;
|
||||
import com.yolo.keyborad.service.KeyboardThemePurchaseService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
@@ -22,17 +24,25 @@ import com.yolo.keyborad.service.KeyboardThemesService;
|
||||
*/
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper, KeyboardThemes> implements KeyboardThemesService {
|
||||
|
||||
private static final String THEME_STYLE_KEY = "theme:style:";
|
||||
private static final String THEME_ALL_KEY = "theme:style:all";
|
||||
|
||||
@Resource
|
||||
@Lazy // 延迟加载,打破循环依赖
|
||||
private KeyboardThemePurchaseService purchaseService;
|
||||
|
||||
@Resource(name = "objectRedisTemplate")
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
|
||||
/**
|
||||
* 根据风格查询主题列表
|
||||
* <p>查询规则:</p>
|
||||
* <ul>
|
||||
* <li>优先从Redis缓存读取主题列表</li>
|
||||
* <li>当themeStyle为9999时,查询所有主题并按排序字段升序排列</li>
|
||||
* <li>其他情况下,查询指定风格的主题</li>
|
||||
* <li>查询结果均过滤已删除和未启用的主题</li>
|
||||
@@ -44,24 +54,45 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
|
||||
* @return 主题列表,包含主题详情和购买状态
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<KeyboardThemesRespVO> selectThemesByStyle(Long themeStyle, Long userId) {
|
||||
// 根据风格参数查询主题列表
|
||||
List<KeyboardThemes> themesList;
|
||||
// 尝试从Redis缓存读取
|
||||
String cacheKey = themeStyle == 9999 ? THEME_ALL_KEY : THEME_STYLE_KEY + themeStyle;
|
||||
List<KeyboardThemesRespVO> themesList = null;
|
||||
|
||||
try {
|
||||
Object cached = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cached != null) {
|
||||
themesList = (List<KeyboardThemesRespVO>) cached;
|
||||
log.debug("从缓存读取风格{}的主题列表,共{}个", themeStyle, themesList.size());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("读取主题缓存失败,将从数据库查询", e);
|
||||
}
|
||||
|
||||
// 缓存未命中,从数据库查询
|
||||
if (themesList == null) {
|
||||
List<KeyboardThemes> themesFromDb;
|
||||
if (themeStyle == 9999) {
|
||||
// 查询所有主题,按排序字段升序
|
||||
themesList = this.lambdaQuery()
|
||||
themesFromDb = this.lambdaQuery()
|
||||
.eq(KeyboardThemes::getDeleted, false)
|
||||
.eq(KeyboardThemes::getThemeStatus, true)
|
||||
.orderByAsc(KeyboardThemes::getSort)
|
||||
.list();
|
||||
} else {
|
||||
// 查询指定风格的主题
|
||||
themesList = this.lambdaQuery()
|
||||
themesFromDb = this.lambdaQuery()
|
||||
.eq(KeyboardThemes::getDeleted, false)
|
||||
.eq(KeyboardThemes::getThemeStatus, true)
|
||||
.eq(KeyboardThemes::getThemeStyle, themeStyle)
|
||||
.list();
|
||||
}
|
||||
themesList = themesFromDb.stream()
|
||||
.map(theme -> BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class))
|
||||
.collect(Collectors.toList());
|
||||
log.debug("从数据库读取风格{}的主题列表,共{}个", themeStyle, themesList.size());
|
||||
}
|
||||
|
||||
// 查询用户已购买的主题ID集合
|
||||
Set<Long> purchasedThemeIds = purchaseService.lambdaQuery()
|
||||
@@ -72,7 +103,7 @@ public class KeyboardThemesServiceImpl extends ServiceImpl<KeyboardThemesMapper,
|
||||
.map(KeyboardThemePurchase::getThemeId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 转换为VO并设置购买状态
|
||||
// 设置购买状态并返回
|
||||
return themesList.stream().map(theme -> {
|
||||
KeyboardThemesRespVO vo = BeanUtil.copyProperties(theme, KeyboardThemesRespVO.class);
|
||||
vo.setIsPurchased(purchasedThemeIds.contains(theme.getId()));
|
||||
|
||||
@@ -77,6 +77,7 @@ public class KeyboardUserInviteCodesServiceImpl extends ServiceImpl<KeyboardUser
|
||||
inviteCode.setExpiresAt(null); // 永久有效
|
||||
inviteCode.setMaxUses(null); // 不限次数
|
||||
inviteCode.setUsedCount(0); // 初始使用次数为0
|
||||
inviteCode.setInviteType("USER"); // 默认为普通用户邀请码
|
||||
|
||||
// 保存到数据库
|
||||
this.save(inviteCode);
|
||||
|
||||
@@ -270,14 +270,23 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
userInvite.setBoundAt(new Date());
|
||||
userInvite.setBindIp(request.getRemoteAddr());
|
||||
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
|
||||
// 记录邀请码类型快照(用户/租户)
|
||||
userInvite.setInviteType(inviteCode.getInviteType());
|
||||
userInvite.setInviteCode(inviteCode.getCode());
|
||||
// 如果是租户邀请码,记录租户ID
|
||||
if ("AGENT".equals(inviteCode.getInviteType()) && inviteCode.getOwnerTenantId() != null) {
|
||||
userInvite.setProfitTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setInviterTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setProfitEmployeeId(inviteCode.getOwnerSystemUserId());
|
||||
}
|
||||
userInvitesService.save(userInvite);
|
||||
|
||||
// 更新邀请码使用次数
|
||||
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
|
||||
inviteCodesService.updateById(inviteCode);
|
||||
|
||||
log.info("User bound to invite code, userId={}, inviteCodeId={}, inviterUserId={}",
|
||||
keyboardUser.getId(), inviteCode.getId(), inviteCode.getOwnerUserId());
|
||||
log.info("User bound to invite code, userId={}, inviteCodeId={}, inviterUserId={}, inviteType={}",
|
||||
keyboardUser.getId(), inviteCode.getId(), inviteCode.getOwnerUserId(), inviteCode.getInviteType());
|
||||
} catch (BusinessException e) {
|
||||
// 邀请码验证失败,记录日志但不影响注册流程
|
||||
log.warn("Failed to bind invite code for user {}: {}", keyboardUser.getId(), e.getMessage());
|
||||
@@ -379,14 +388,23 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
||||
userInvite.setBoundAt(new Date());
|
||||
userInvite.setBindIp(request.getRemoteAddr());
|
||||
userInvite.setBindUserAgent(request.getHeader("User-Agent"));
|
||||
// 记录邀请码类型快照(用户/租户)
|
||||
userInvite.setInviteType(inviteCode.getInviteType());
|
||||
userInvite.setInviteCode(inviteCode.getCode());
|
||||
// 如果是租户邀请码,记录租户ID
|
||||
if ("AGENT".equals(inviteCode.getInviteType()) && inviteCode.getOwnerTenantId() != null) {
|
||||
userInvite.setProfitTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setInviterTenantId(inviteCode.getOwnerTenantId());
|
||||
userInvite.setProfitEmployeeId(inviteCode.getOwnerSystemUserId());
|
||||
}
|
||||
userInvitesService.save(userInvite);
|
||||
|
||||
// 更新邀请码使用次数
|
||||
inviteCode.setUsedCount(inviteCode.getUsedCount() + 1);
|
||||
inviteCodesService.updateById(inviteCode);
|
||||
|
||||
log.info("User bound invite code, userId={}, inviteCodeId={}, inviterUserId={}",
|
||||
userId, inviteCode.getId(), inviteCode.getOwnerUserId());
|
||||
log.info("User bound invite code, userId={}, inviteCodeId={}, inviterUserId={}, inviteType={}",
|
||||
userId, inviteCode.getId(), inviteCode.getOwnerUserId(), inviteCode.getInviteType());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user