From 384bce80449b17651937429038c4370dbce60e7b Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期二, 13 十月 2026 15:12:22 +0800
Subject: [PATCH] Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/java/Hugray
---
hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 252 insertions(+), 9 deletions(-)
diff --git a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
index ebd2a43..47b2cec 100644
--- a/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
+++ b/hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java
@@ -1,19 +1,36 @@
package com.ruoyi.user.service.impl;
+import java.util.Collections;
+import java.util.Date;
import java.security.SecureRandom;
import java.util.List;
+import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.core.constant.UserConstants;
+import com.ruoyi.common.core.exception.ServiceException;
+import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.utils.SecurityUtils;
+import com.ruoyi.user.domain.AccountAddress;
import com.ruoyi.user.config.UniversalCodeProperties;
import com.ruoyi.user.constant.ReferralConstants;
import com.ruoyi.user.domain.AccountUser;
+import com.ruoyi.user.domain.vo.AccountUserDetailVo;
+import com.ruoyi.user.domain.vo.MemberOptionVo;
+import com.ruoyi.user.mapper.AccountAddressMapper;
+import com.ruoyi.user.mapper.AccountAgreementMapper;
+import com.ruoyi.user.mapper.AccountCertificationMapper;
+import com.ruoyi.user.domain.vo.AccountUserInfoVo;
+import com.ruoyi.user.domain.vo.EarningSummaryVo;
+import com.ruoyi.user.enums.MemberLevelEnum;
+import com.ruoyi.user.mapper.AccountEarningRecordMapper;
import com.ruoyi.user.mapper.AccountUserMapper;
+import com.ruoyi.user.mapper.AccountWarehouseMapper;
import com.ruoyi.user.service.IAccountUserService;
/**
@@ -28,6 +45,21 @@
@Autowired
private AccountUserMapper accountUserMapper;
+
+ @Autowired
+ private AccountCertificationMapper accountCertificationMapper;
+
+ @Autowired
+ private AccountAddressMapper accountAddressMapper;
+
+ @Autowired
+ private AccountAgreementMapper accountAgreementMapper;
+
+ @Autowired
+ private AccountWarehouseMapper accountWarehouseMapper;
+
+ @Autowired
+ private AccountEarningRecordMapper accountEarningRecordMapper;
@Autowired
private UniversalCodeProperties universalCodeProperties;
@@ -111,6 +143,167 @@
}
@Override
+ public AccountUserDetailVo selectAccountUserDetail(Long userId)
+ {
+ AccountUserDetailVo detail = new AccountUserDetailVo();
+ detail.setUser(accountUserMapper.selectAccountUserVoById(userId));
+ detail.setCertification(accountCertificationMapper.selectCertificationByUserId(userId));
+ detail.setAddresses(accountAddressMapper.selectAddressByUserId(userId));
+ detail.setAgreements(accountAgreementMapper.selectAgreementByUserId(userId));
+ detail.setWarehouses(accountWarehouseMapper.selectWarehouseByUserId(userId));
+ return detail;
+ }
+
+ @Override
+ public int updateAccountUserRelation(AccountUser accountUser)
+ {
+ AccountUser old = accountUserMapper.selectAccountUserById(accountUser.getUserId());
+ if (StringUtils.isNull(old))
+ {
+ return 0;
+ }
+ validateSupplier(old, accountUser.getSupplierId());
+ // 绑定时间由服务端推导:换成另一个人的记为当前时间,解绑则清空,
+ // 关系没动则保留原值——否则每次保存都会把绑定时间刷成今天。
+ Date now = DateUtils.getNowDate();
+ accountUser.setReferrerBindTime(
+ resolveBindTime(old.getReferrerId(), accountUser.getReferrerId(), old.getReferrerBindTime(), now));
+ accountUser.setSupplierBindTime(
+ resolveBindTime(old.getSupplierId(), accountUser.getSupplierId(), old.getSupplierBindTime(), now));
+ return accountUserMapper.updateUserRelation(accountUser);
+ }
+
+ @Override
+ public List<MemberOptionVo> searchSupplierCandidates(Long userId, String keyword)
+ {
+ // 空关键字不回结果:候选是等级高于该会员的全量集合,放开等于把大半张会员表倒进下拉。
+ // 与 AccountCityCompanyServiceImpl.searchL4Member 是同一口径(那边搜 L4,这边搜等级高于该会员)。
+ if (StringUtils.isEmpty(keyword))
+ {
+ return Collections.emptyList();
+ }
+ AccountUser member = accountUserMapper.selectAccountUserById(userId);
+ if (StringUtils.isNull(member))
+ {
+ throw new ServiceException("会员不存在");
+ }
+ // 严格高于该会员的等级集合;该会员已是最高等级时为空,必须提前返回——
+ // 空集合会让候选 SQL 拼出 in () 的语法错误。
+ List<String> levels = AccountUser.levelsAbove(member.getSystemLevel());
+ if (levels.isEmpty())
+ {
+ return Collections.emptyList();
+ }
+ String kw = keyword.trim();
+ // 纯数字时补一个 keywordId 走等值匹配(会员编号),非数字则不参与该列比较——
+ // 写成 user_id like 或 cast(user_id as char) 都会让主键索引用不上。同 searchL4Member。
+ Long keywordId = null;
+ try
+ {
+ keywordId = Long.valueOf(kw);
+ }
+ catch (NumberFormatException ignored)
+ {
+ // 非纯数字:只走手机号/昵称/姓名模糊匹配
+ }
+ return accountUserMapper.selectSupplierCandidates(userId, levels, kw, keywordId);
+ }
+
+ /**
+ * 校验新供货人的等级必须严格高于该会员(C < L1 < L2 < L3 < L4)。
+ *
+ * 这是提交时的第二道闸:弹窗只列出合法候选,但 /account/user/relation 是公开入口,
+ * 绕过页面直接打接口也得拦住。解除供货(传 null)不需要校验。
+ *
+ * 与旧值相同时直接放行:存量关系是「历来没有等级限制」时期写下的(例如 L4 的供货人是 C),
+ * 不该因为这次没换人被卡死——那是另一件事,按本项目的规矩也不在这里顺手修历史数据。
+ */
+ private void validateSupplier(AccountUser old, Long newSupplierId)
+ {
+ if (newSupplierId == null || newSupplierId.equals(old.getSupplierId()))
+ {
+ return;
+ }
+ // selectAccountUserById 的 where 自带 del_flag = '0',查不到即「不存在或已删除」
+ AccountUser supplier = accountUserMapper.selectAccountUserById(newSupplierId);
+ if (StringUtils.isNull(supplier))
+ {
+ throw new ServiceException("新供货人不存在");
+ }
+ if (!AccountUser.isHigherLevel(supplier.getSystemLevel(), old.getSystemLevel()))
+ {
+ throw new ServiceException("新供货人的系统等级必须高于该会员(该会员:" + old.getSystemLevel()
+ + ",所选:" + supplier.getSystemLevel() + ")");
+ }
+ }
+
+ @Override
+ public int changeStatus(AccountUser accountUser)
+ {
+ // 本接口只服务列表页的「禁用/启用」,故目标状态只允许「正常/禁用」两态。
+ // 注销中、已注销必须由注销审核流程驱动(见 AccountCancelApplyServiceImpl):
+ // 若这里放开,「已注销」的会员一次普通请求就能被改回「正常」,等于绕过审核。
+ String target = accountUser.getStatus();
+ if (!AccountUser.STATUS_NORMAL.equals(target) && !AccountUser.STATUS_DISABLED.equals(target))
+ {
+ throw new ServiceException("账户状态只能改为「正常」或「禁用」");
+ }
+ AccountUser old = accountUserMapper.selectAccountUserById(accountUser.getUserId());
+ if (StringUtils.isNull(old))
+ {
+ throw new ServiceException("会员不存在");
+ }
+ // 反向也要拦:注销中/已注销的会员不允许从列表页改回正常或禁用,否则审核结论形同虚设
+ if (AccountUser.STATUS_CANCELING.equals(old.getStatus()) || AccountUser.STATUS_CANCELED.equals(old.getStatus()))
+ {
+ throw new ServiceException("该会员正在注销或已注销,账户状态只能由注销审核流程变更");
+ }
+ return accountUserMapper.updateUserStatus(accountUser);
+ }
+
+ @Override
+ @Transactional
+ public int updateAccountAddress(AccountAddress accountAddress)
+ {
+ // 置为默认前先清掉同会员其它行的默认标记:MySQL 没有部分唯一索引,
+ // "每会员至多一条 is_default='1'"只能靠这两条语句在同一事务内保证。
+ if ("1".equals(accountAddress.getIsDefault()))
+ {
+ accountAddressMapper.clearDefaultByUserId(accountAddress.getUserId());
+ }
+ int rows = accountAddressMapper.updateAddress(accountAddress);
+ // updateAddress 的 where 带 user_id,id 与 userId 对不上时会更新 0 行。
+ // 此时上面的默认标记已被清掉,必须抛异常回滚,
+ // 否则该会员会落到"一个默认地址都没有"的状态。
+ if (rows == 0)
+ {
+ throw new ServiceException("收货地址不存在或不属于该会员");
+ }
+ return rows;
+ }
+
+ @Override
+ public int deleteAccountAddressByIds(Long[] ids)
+ {
+ return accountAddressMapper.deleteAddressByIds(ids);
+ }
+
+
+ private Date resolveBindTime(Long oldId, Long newId, Date oldTime, Date now)
+ {
+ if (newId == null)
+ {
+ return null;
+ }
+ // 关系未变时沿用原绑定时间;oldTime 为空(历史数据没记)则补当前时间
+ if (Objects.equals(oldId, newId) && oldTime != null)
+ {
+ return oldTime;
+ }
+ return now;
+ }
+
+ @Override
public AccountUser registerMember(String phone, String referralCode)
{
// STEP 1: 二次校验手机号未注册(防并发 / 防重复提交,网关侧已用分布式锁兜底)
@@ -118,24 +311,74 @@
{
throw new ServiceException("手机号已注册,请前往登录");
}
- // STEP 2: 解析推荐人——命中通用推荐码则不绑定推荐人,否则按码定位并校验
- Long referrerUserId = resolveReferrerUserId(phone, referralCode);
- // STEP 3: 生成全局唯一的本人推荐码(库内查重,冲突重试)
+ // STEP 2: 判定是否命中通用推荐码(命中则不绑定推荐人,且直接发放 L4 会员等级)
+ boolean universalReferral = isUniversalReferralCode(referralCode);
+ // STEP 3: 解析推荐人——命中通用推荐码则不绑定推荐人,否则按码定位并校验
+ Long referrerUserId = resolveReferrerUserId(phone, referralCode, universalReferral);
+ // STEP 4: 生成全局唯一的本人推荐码(库内查重,冲突重试)
String myReferralCode = generateUniqueReferralCode();
- // STEP 4: 组装会员信息(C端无密码,账号=手机号),落库(userId 回填)
+ // STEP 5: 组装会员信息(C端无密码,账号=手机号;姓名注册未采集,落库留空,由用户后续在资料页完善)
AccountUser accountUser = new AccountUser();
accountUser.setUserName(phone);
accountUser.setPhone(phone);
- accountUser.setNickname(phone);
accountUser.setStatus(UserConstants.NORMAL);
accountUser.setMyReferralCode(myReferralCode);
- accountUser.setReferrerUserId(referrerUserId);
+ accountUser.setReferrerId(referrerUserId);
+ // 通用推荐码注册的会员直接发放 L4 等级;其余不设等级,走库默认值 C(普通用户)
+ if (universalReferral)
+ {
+ accountUser.setSystemLevel(MemberLevelEnum.L4.getCode());
+ }
accountUser.setCreateBy(phone);
if (insertAccountUser(accountUser) <= 0)
{
throw new ServiceException("注册失败,请稍后重试");
}
return accountUser;
+ }
+
+ @Override
+ public AccountUserInfoVo getAccountUserInfo(Long userId)
+ {
+ // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询)
+ if (StringUtils.isNull(userId))
+ {
+ throw new ServiceException("获取会员信息失败");
+ }
+ // STEP 2: 按会员ID查询会员(逻辑删除已在 SQL 中过滤)
+ AccountUser accountUser = accountUserMapper.selectAccountUserById(userId);
+ if (StringUtils.isNull(accountUser))
+ {
+ throw new ServiceException("获取会员信息失败");
+ }
+ // STEP 3: 查询收益汇总(单位分),无任何流水时直接返回全零,避免前端 null 分支
+ EarningSummaryVo earnings = accountEarningRecordMapper.selectEarningSummary(userId);
+ if (StringUtils.isNull(earnings))
+ {
+ earnings = EarningSummaryVo.zero();
+ }
+ // STEP 4: 组装展示对象;等级为空或非法值统一兜底为 C(普通用户);手机号脱敏后下发
+ AccountUserInfoVo vo = new AccountUserInfoVo();
+ vo.setUserId(accountUser.getUserId());
+ vo.setNickname(accountUser.getNickname());
+ vo.setPhone(maskPhone(accountUser.getPhone()));
+ vo.setAvatar(accountUser.getAvatar());
+ vo.setMemberLevel(MemberLevelEnum.normalize(accountUser.getSystemLevel()));
+ vo.setEarnings(earnings);
+ // STEP 5: 返回个人中心展示数据
+ return vo;
+ }
+
+ /**
+ * 判定是否为通用推荐码
+ *
+ * @param referralCode 注册提交的推荐码
+ * @return true 表示命中通用推荐码
+ */
+ private boolean isUniversalReferralCode(String referralCode)
+ {
+ String universalCode = universalCodeProperties.getUniversalReferralCode();
+ return StringUtils.isNotEmpty(universalCode) && universalCode.equals(referralCode);
}
/**
@@ -146,13 +389,13 @@
*
* @param phone 注册手机号(仅用于日志留痕,脱敏输出)
* @param referralCode 推荐码
+ * @param universalReferral 是否已命中通用推荐码
* @return 推荐人用户ID;命中通用码时为 null
*/
- private Long resolveReferrerUserId(String phone, String referralCode)
+ private Long resolveReferrerUserId(String phone, String referralCode, boolean universalReferral)
{
- String universalCode = universalCodeProperties.getUniversalReferralCode();
// 命中通用推荐码:不绑定推荐人
- if (StringUtils.isNotEmpty(universalCode) && universalCode.equals(referralCode))
+ if (universalReferral)
{
log.warn("[REFERRAL] universal code hit, not binding referrer, phone={}", maskPhone(phone));
return null;
--
Gitblit v1.7.1