feat(account): 添加账户扣费系数配置和邀请码功能

- 在AccountFrozenController中添加跨域注解并修改请求路径为/api/accountFrozen
- 引入AccountDeductionProperties配置类,支持动态扣费系数设置
- 修改账户冻结逻辑,将原来的1元=100积分改为1分=1积分,并应用扣费系数
- 在SysUser实体中添加inviteCode、invitedCode、invitedBy字段
- 实现用户注册时自动生成邀请码功能
- 添加邀请码验证和奖励机制,邀请成功赠送100积分
- 优化PhoneLoginDto添加邀请码参数
- 完善数据库映射文件支持新字段操作
This commit is contained in:
wangzhiwei 2026-04-13 14:50:13 +08:00
parent 71edec48f7
commit 7841b94872
9 changed files with 193 additions and 28 deletions

View File

@ -0,0 +1,26 @@
package com.kexue.skills.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 账户扣费配置属性
*
* @author 系统生成
* @since 2026-04-13
*/
@Data
@Component
public class AccountDeductionProperties {
/**
* 扣费系数默认2倍
* 例如系数为2时实际消耗1积分扣除2积分
*/
@Value("${account.deduction.coefficient:2}")
private BigDecimal coefficient;
}

View File

@ -11,10 +11,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
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 org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
@ -26,7 +23,8 @@ import java.io.IOException;
* @since 2026-04-11
*/
@RestController
@RequestMapping("/accountFrozen")
@RequestMapping("/api/accountFrozen")
@CrossOrigin(origins = "*")
@Tag(name = "账户冻结单", description = "账户冻结单管理接口")
public class AccountFrozenController {

View File

@ -66,4 +66,13 @@ public class SysUser extends BaseEntity implements Serializable {
@Schema(description ="会话ID")
private String sessionId;
@Schema(description ="邀请码(用于邀请别人)")
private String inviteCode;
@Schema(description ="被邀请码(邀请我注册的邀请码)")
private String invitedCode;
@Schema(description ="邀请人用户ID邀请我注册的用户ID")
private Long invitedBy;
}

View File

@ -23,4 +23,7 @@ public class PhoneLoginDto implements Serializable {
@Schema(description ="验证码")
private String code;
@Schema(description ="邀请码")
private String inviteCode;
}

View File

@ -88,4 +88,6 @@ public interface SysUserMapper {
SysUser getBySessionId(String sessionId);
SysUser getByInviteCode(String inviteCode);
}

View File

@ -16,6 +16,7 @@ import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.mapper.AccountTransactionMapper;
import com.kexue.skills.common.util.IDUtils;
import com.kexue.skills.common.ResultCode;
import com.kexue.skills.config.AccountDeductionProperties;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -48,6 +49,9 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
@Resource
private AccountTransactionMapper accountTransactionMapper;
@Resource
private AccountDeductionProperties accountDeductionProperties;
/**
* 创建冻结单
* @param accountFrozenDto 冻结单DTO
@ -101,11 +105,12 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
}
// 总费用
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 转换为
BigDecimal feeInYuan = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100));
// 转换为积分1元=100积分
finalFrozenAmount = feeInYuan.multiply(BigDecimal.valueOf(100));
// 转换为积分1分=1积分无需转换
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
// 应用扣费系数
finalFrozenAmount = baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
}
}
@ -123,7 +128,7 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
account.setUpdateTime(new Date());
accountMapper.update(account);
// 6. 创建冻结单
// 6. 创建冻结单单位积分
AccountFrozen accountFrozen = new AccountFrozen();
accountFrozen.setFrozenId(IDUtils.getSnowflakeIdStr());
accountFrozen.setUserId(userId);
@ -199,16 +204,17 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
}
// 总费用
// 注意因为1分=1积分所以totalFee直接就是积分数量
long totalFee = inputFee + outputFee;
// 转换为
BigDecimal feeInYuan = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100));
// 转换为积分1元=100积分
finalAmount = feeInYuan.multiply(BigDecimal.valueOf(100));
// 转换为积分1分=1积分无需转换
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
// 应用扣费系数
finalAmount = baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
}
}
// 7. 更新账户余额和冻结金额
// 7. 更新账户余额和冻结金额单位积分
BigDecimal frozenAmount = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
BigDecimal beforeBalance = balance;
@ -240,7 +246,7 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
account.setUpdateTime(new Date());
accountMapper.update(account);
// 8. 生成流水记录
// 8. 生成流水记录单位积分
if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(accountFrozen.getUserId());
@ -271,7 +277,7 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
accountTransactionMapper.insert(transaction);
}
// 9. 更新冻结单状态
// 9. 更新冻结单状态单位积分
accountFrozen.setFinalAmount(finalAmount);
accountFrozen.setUsageInputTokens(accountReleaseDto.getUsageInputTokens());
accountFrozen.setUsageOutputTokens(accountReleaseDto.getUsageOutputTokens());

View File

@ -27,6 +27,7 @@ import com.kexue.skills.mapper.CmsContentMapper;
import com.kexue.skills.mapper.SysUserMapper;
import com.kexue.skills.mapper.SysUserRoleMapper;
import com.kexue.skills.service.SysUserService;
import com.kexue.skills.service.AccountService;
import com.kexue.skills.utils.MD5Util;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
@ -77,6 +78,9 @@ public class SysUserServiceImpl implements SysUserService {
@Resource
private CmsContentMapper cmsContentMapper;
@Resource
private AccountService accountService;
/**
* 分页查询数据
*
@ -138,6 +142,34 @@ public class SysUserServiceImpl implements SysUserService {
sysUser.setDeleteFlag(Const.DELETE_FLAG_NO);
//写一个salt生成方法
sysUser.setSalt(System.currentTimeMillis()+"");
// 生成随机邀请码8位字母数字组合
if (sysUser.getInviteCode() == null || sysUser.getInviteCode().isEmpty()) {
sysUser.setInviteCode(generateInviteCode());
}
// 处理邀请码
if (sysUser.getInvitedCode() != null && !sysUser.getInvitedCode().isEmpty()) {
// 验证邀请码是否有效
SysUser inviter = sysUserMapper.getByInviteCode(sysUser.getInvitedCode());
if (inviter == null) {
throw new BizException("邀请码无效");
}
// 设置邀请人用户ID
sysUser.setInvitedBy(inviter.getUserId());
// 给邀请人赠送100积分
accountService.addGiftBalance(
inviter.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"invite",
"邀请用户注册赠送"
);
}
try {
// 假设客户端已经对密码进行了一次MD5加密服务端使用双重加密验证
String encryptedPwd = MD5Util.doubleEncrypt(sysUser.getPwd(), sysUser.getSalt());
@ -157,6 +189,16 @@ public class SysUserServiceImpl implements SysUserService {
account.setDeleteFlag(Const.DELETE_FLAG_NO); // 初始未删除
accountMapper.insert(account);
// 注册赠送100积分
accountService.addGiftBalance(
sysUser.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"register",
"注册赠送"
);
// 将返回的用户密码设置为null
sysUser.setPwd(null);
return sysUser;
@ -857,7 +899,7 @@ public class SysUserServiceImpl implements SysUserService {
// 如果用户不存在自动创建账号
if (sysUser == null) {
sysUser = createUserByPhone(phone);
sysUser = createUserByPhone(phone, phoneLoginDto.getInviteCode());
}
// 检查用户是否已有 token如果有则使旧 token 失效
@ -954,15 +996,42 @@ public class SysUserServiceImpl implements SysUserService {
* 根据手机号创建用户
*
* @param phone 手机号
* @param inviteCode 邀请码
* @return 创建的用户对象
*/
private SysUser createUserByPhone(String phone) {
private SysUser createUserByPhone(String phone, String inviteCode) {
SysUser sysUser = new SysUser();
// 设置用户名使用手机号作为用户名
sysUser.setUserName(phone);
sysUser.setTel(phone);
// 生成随机邀请码8位字母数字组合
String generatedInviteCode = generateInviteCode();
sysUser.setInviteCode(generatedInviteCode);
// 处理邀请码
if (inviteCode != null && !inviteCode.isEmpty()) {
// 验证邀请码是否有效
SysUser inviter = sysUserMapper.getByInviteCode(inviteCode);
if (inviter == null) {
throw new BizException("邀请码无效");
}
// 设置被邀请码和邀请人用户ID
sysUser.setInvitedCode(inviteCode);
sysUser.setInvitedBy(inviter.getUserId());
// 给邀请人赠送100积分
accountService.addGiftBalance(
inviter.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"invite",
"邀请用户注册赠送"
);
}
// 设置固定salt为666666
String salt = "666666";
sysUser.setSalt(salt);
@ -994,9 +1063,34 @@ public class SysUserServiceImpl implements SysUserService {
account.setDeleteFlag(Const.DELETE_FLAG_NO);
accountMapper.insert(account);
// 注册赠送100积分
accountService.addGiftBalance(
sysUser.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"register",
"注册赠送"
);
return sysUser;
}
/**
* 生成8位邀请码
*
* @return 邀请码
*/
private String generateInviteCode() {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(characters.charAt(random.nextInt(characters.length())));
}
return sb.toString();
}
/**
* 生成随机密码
*

View File

@ -98,3 +98,9 @@ web:
snowflake:
workid: 1 # 机器ID分布式部署时需要保证唯一
# 账户扣费配置
account:
deduction:
# 扣费系数默认2倍实际消耗1积分扣除2积分
coefficient: 2

View File

@ -15,12 +15,15 @@
<result property="enable" column="enable" jdbcType="OTHER"/>
<result property="deleteFlag" column="delete_flag" jdbcType="OTHER"/>
<result property="sessionId" column="session_id" jdbcType="VARCHAR"/>
<result property="inviteCode" column="invite_code" jdbcType="VARCHAR"/>
<result property="invitedCode" column="invited_code" jdbcType="VARCHAR"/>
<result property="invitedBy" column="invited_by" jdbcType="BIGINT"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
where user_id = #{userId}
</select>
@ -28,7 +31,7 @@
<!--查询指定行数据-->
<select id="getPageList" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
<where>
<if test="userId != null">
@ -70,7 +73,7 @@
<!--查询指定行数据-->
<select id="queryAllByLimit" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
limit #{offset}, #{limit}
</select>
@ -78,7 +81,7 @@
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
<where>
<if test="userId != null">
@ -119,8 +122,8 @@
<!--新增所有列-->
<insert id="insert" keyProperty="userId" useGeneratedKeys="true">
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId})
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId}, #{inviteCode}, #{invitedCode}, #{invitedBy})
</insert>
@ -182,6 +185,15 @@
<if test="sessionId != null and sessionId != ''">
session_id = #{sessionId},
</if>
<if test="inviteCode != null and inviteCode != ''">
invite_code = #{inviteCode},
</if>
<if test="invitedCode != null and invitedCode != ''">
invited_code = #{invitedCode},
</if>
<if test="invitedBy != null">
invited_by = #{invitedBy},
</if>
</set>
where user_id = #{userId}
</update>
@ -193,7 +205,7 @@
<select id="getByUsername" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
where (user_name = #{userName} or tel = #{userName})
and delete_flag = 0
@ -202,7 +214,7 @@
<select id="getByTel" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
where tel = #{tel}
and delete_flag = 0
@ -210,11 +222,20 @@
</select>
<select id="getBySessionId" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
where session_id = #{sessionId}
and delete_flag = 0
limit 1
</select>
<select id="getByInviteCode" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
from sys_user
where invite_code = #{inviteCode}
and delete_flag = 0
limit 1
</select>
</mapper>