From 7841b94872fb703ebb6c98becee2ba324550a6aa Mon Sep 17 00:00:00 2001 From: wangzhiwei Date: Mon, 13 Apr 2026 14:50:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E6=B7=BB=E5=8A=A0=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E6=89=A3=E8=B4=B9=E7=B3=BB=E6=95=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E9=82=80=E8=AF=B7=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AccountFrozenController中添加跨域注解并修改请求路径为/api/accountFrozen - 引入AccountDeductionProperties配置类,支持动态扣费系数设置 - 修改账户冻结逻辑,将原来的1元=100积分改为1分=1积分,并应用扣费系数 - 在SysUser实体中添加inviteCode、invitedCode、invitedBy字段 - 实现用户注册时自动生成邀请码功能 - 添加邀请码验证和奖励机制,邀请成功赠送100积分 - 优化PhoneLoginDto添加邀请码参数 - 完善数据库映射文件支持新字段操作 --- .../config/AccountDeductionProperties.java | 26 +++++ .../controller/AccountFrozenController.java | 8 +- .../java/com/kexue/skills/entity/SysUser.java | 9 ++ .../skills/entity/request/PhoneLoginDto.java | 3 + .../kexue/skills/mapper/SysUserMapper.java | 2 + .../impl/AccountFrozenServiceImpl.java | 30 +++--- .../service/impl/SysUserServiceImpl.java | 98 ++++++++++++++++++- src/main/resources/application.yml | 6 ++ src/main/resources/mapper/SysUserMapper.xml | 39 ++++++-- 9 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/kexue/skills/config/AccountDeductionProperties.java diff --git a/src/main/java/com/kexue/skills/config/AccountDeductionProperties.java b/src/main/java/com/kexue/skills/config/AccountDeductionProperties.java new file mode 100644 index 0000000..2a295b7 --- /dev/null +++ b/src/main/java/com/kexue/skills/config/AccountDeductionProperties.java @@ -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; + +} diff --git a/src/main/java/com/kexue/skills/controller/AccountFrozenController.java b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java index 49c659a..a300777 100644 --- a/src/main/java/com/kexue/skills/controller/AccountFrozenController.java +++ b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java @@ -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 { diff --git a/src/main/java/com/kexue/skills/entity/SysUser.java b/src/main/java/com/kexue/skills/entity/SysUser.java index cc720ff..cb336e0 100644 --- a/src/main/java/com/kexue/skills/entity/SysUser.java +++ b/src/main/java/com/kexue/skills/entity/SysUser.java @@ -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; + } diff --git a/src/main/java/com/kexue/skills/entity/request/PhoneLoginDto.java b/src/main/java/com/kexue/skills/entity/request/PhoneLoginDto.java index 885bce7..7f281fe 100644 --- a/src/main/java/com/kexue/skills/entity/request/PhoneLoginDto.java +++ b/src/main/java/com/kexue/skills/entity/request/PhoneLoginDto.java @@ -23,4 +23,7 @@ public class PhoneLoginDto implements Serializable { @Schema(description ="验证码") private String code; + + @Schema(description ="邀请码") + private String inviteCode; } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/mapper/SysUserMapper.java b/src/main/java/com/kexue/skills/mapper/SysUserMapper.java index c9e4927..f58b69e 100644 --- a/src/main/java/com/kexue/skills/mapper/SysUserMapper.java +++ b/src/main/java/com/kexue/skills/mapper/SysUserMapper.java @@ -88,4 +88,6 @@ public interface SysUserMapper { SysUser getBySessionId(String sessionId); + SysUser getByInviteCode(String inviteCode); + } diff --git a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java index 38c104d..b3b8154 100644 --- a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java @@ -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()); diff --git a/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java index 2eb466a..e4e1a16 100644 --- a/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java @@ -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; @@ -76,6 +77,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(); + } + /** * 生成随机密码 * diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2a8738c..91f4fd0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -98,3 +98,9 @@ web: snowflake: workid: 1 # 机器ID,分布式部署时需要保证唯一 +# 账户扣费配置 +account: + deduction: + # 扣费系数,默认2倍(实际消耗1积分,扣除2积分) + coefficient: 2 + diff --git a/src/main/resources/mapper/SysUserMapper.xml b/src/main/resources/mapper/SysUserMapper.xml index efde24b..3c0bc9a 100644 --- a/src/main/resources/mapper/SysUserMapper.xml +++ b/src/main/resources/mapper/SysUserMapper.xml @@ -15,12 +15,15 @@ + + + @@ -28,7 +31,7 @@ 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} @@ -78,7 +81,7 @@ 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 @@ + +