From a4afcfdb179e62a498b62cc6860d8cd46701a08f Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 14 十月 2026 09:53:47 +0800
Subject: [PATCH] feat(user, miniapp): 完成C端小程序收货地址完整功能闭环

---
 hugray-server/ruoyi-modules/ruoyi-user/src/main/java/com/ruoyi/user/service/impl/AccountUserServiceImpl.java |  203 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 202 insertions(+), 1 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 47b2cec..a26c589 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,5 +1,6 @@
 package com.ruoyi.user.service.impl;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.security.SecureRandom;
@@ -13,14 +14,16 @@
 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.AccountInviteeVo;
+import com.ruoyi.user.domain.vo.AccountReferrerVo;
 import com.ruoyi.user.domain.vo.AccountUserDetailVo;
+import com.ruoyi.user.domain.vo.InviteCodeVo;
 import com.ruoyi.user.domain.vo.MemberOptionVo;
 import com.ruoyi.user.mapper.AccountAddressMapper;
 import com.ruoyi.user.mapper.AccountAgreementMapper;
@@ -66,6 +69,12 @@
 
     /** 推荐码随机源(SecureRandom 线程安全,避免每次创建) */
     private static final SecureRandom RANDOM = new SecureRandom();
+
+    /** 头像地址最大长度(对应 account_user.avatar varchar(255),超长直接拒绝避免截断) */
+    private static final int AVATAR_MAX_LENGTH = 255;
+
+    /** 认证状态:未认证(auth_status 为空时的兜底值) */
+    private static final String AUTH_STATUS_NONE = "0";
 
     @Override
     public AccountUser selectAccountUserById(Long userId)
@@ -288,6 +297,67 @@
         return accountAddressMapper.deleteAddressByIds(ids);
     }
 
+    @Override
+    public List<AccountAddress> getAddressList(Long userId)
+    {
+        return accountAddressMapper.selectAddressByUserId(userId);
+    }
+
+    @Override
+    public AccountAddress getAddressDetail(Long userId, Long id)
+    {
+        // 编辑页回填:需校验归属,防止拿到他人地址
+        return queryOwnedAddress(userId, id);
+    }
+
+    @Override
+    @Transactional
+    public int insertMyAddress(AccountAddress accountAddress)
+    {
+        // STEP 1: 会员身份防御性校验(正常由登录态注入,必非空)
+        if (accountAddress.getUserId() == null)
+        {
+            throw new ServiceException("会员身份缺失");
+        }
+        // STEP 2: 是否默认完全以用户勾选为准,不做「首条自动默认」的强制改写;
+        //         为空时明确兜底为 0,避免落库为 null 影响前端判定
+        if (!"1".equals(accountAddress.getIsDefault()))
+        {
+            accountAddress.setIsDefault("0");
+        }
+        // STEP 3: 置为默认前先清掉旧默认,保证"每会员至多一条默认"(与插入同事务)
+        if ("1".equals(accountAddress.getIsDefault()))
+        {
+            accountAddressMapper.clearDefaultByUserId(accountAddress.getUserId());
+        }
+        // STEP 4: 落库
+        return accountAddressMapper.insertAddress(accountAddress);
+    }
+
+    @Override
+    @Transactional
+    public int deleteMyAddress(Long userId, Long id)
+    {
+        // 校验归属:非本人地址直接抛异常,避免越权删除(deleteAddressByIds 本身不带 user_id)
+        queryOwnedAddress(userId, id);
+        return accountAddressMapper.deleteAddressByIds(new Long[]{ id });
+    }
+
+    /**
+     * 查询地址并校验归属
+     *
+     * 地址不存在或不属于该会员时统一抛业务异常(避免暴露他人地址是否存在)。
+     */
+    private AccountAddress queryOwnedAddress(Long userId, Long id)
+    {
+        AccountAddress address = accountAddressMapper.selectAddressById(id);
+        if (address == null || !Objects.equals(address.getUserId(), userId))
+        {
+            throw new ServiceException("收货地址不存在或不属于该会员");
+        }
+        return address;
+    }
+
 
     private Date resolveBindTime(Long oldId, Long newId, Date oldTime, Date now)
     {
@@ -364,11 +434,142 @@
         vo.setPhone(maskPhone(accountUser.getPhone()));
         vo.setAvatar(accountUser.getAvatar());
         vo.setMemberLevel(MemberLevelEnum.normalize(accountUser.getSystemLevel()));
+        // 认证状态为空(历史数据)时兜底为「未认证」,避免前端徽标无值可判
+        vo.setAuthStatus(StringUtils.isEmpty(accountUser.getAuthStatus())
+            ? AUTH_STATUS_NONE : accountUser.getAuthStatus());
+        // 企业/个体认证状态同样兜底为「未认证」(与个人认证状态相互独立)
+        vo.setCompanyAuthStatus(StringUtils.isEmpty(accountUser.getCompanyAuthStatus())
+            ? AUTH_STATUS_NONE : accountUser.getCompanyAuthStatus());
         vo.setEarnings(earnings);
         // STEP 5: 返回个人中心展示数据
         return vo;
     }
 
+    @Override
+    public void updateAvatar(Long userId, String avatarUrl)
+    {
+        // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属更新)
+        if (StringUtils.isNull(userId))
+        {
+            throw new ServiceException("更新头像失败");
+        }
+        // STEP 2: 校验头像地址非空
+        if (StringUtils.isEmpty(avatarUrl))
+        {
+            throw new ServiceException("头像地址不能为空");
+        }
+        // STEP 3: 校验长度,超出列长直接拒绝,避免落库截断
+        if (avatarUrl.length() > AVATAR_MAX_LENGTH)
+        {
+            throw new ServiceException("头像地址过长");
+        }
+        // STEP 4: 只更新 avatar 一列(单列语句,不触碰其它资料)
+        AccountUser update = new AccountUser();
+        update.setUserId(userId);
+        update.setAvatar(avatarUrl);
+        update.setUpdateBy(SecurityUtils.getUsername());
+        // STEP 5: 影响行数为 0 说明会员不存在或已删除(SQL 自带 del_flag='0' 过滤)
+        if (accountUserMapper.updateUserAvatar(update) == 0)
+        {
+            throw new ServiceException("更新头像失败");
+        }
+    }
+
+    @Override
+    public InviteCodeVo getInviteCode(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: 组装展示对象(邀请码注册时生成,C 端会员必有)
+        InviteCodeVo vo = new InviteCodeVo();
+        vo.setReferralCode(accountUser.getMyReferralCode());
+        return vo;
+    }
+
+    @Override
+    public List<AccountInviteeVo> getInvitees(Long userId)
+    {
+        // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询)
+        if (StringUtils.isNull(userId))
+        {
+            throw new ServiceException("获取邀请列表失败");
+        }
+        // STEP 2: 按 referrer_id 查询直接下级,SQL 已按绑定时间倒序(配合 Controller 分页)
+        List<AccountUser> invitees = accountUserMapper.selectInvitees(userId);
+        // STEP 3: 逐条组装展示对象;手机号脱敏、绑定日格式化,等级原值直出
+        List<AccountInviteeVo> result = new ArrayList<>(invitees.size());
+        for (AccountUser invitee : invitees)
+        {
+            AccountInviteeVo vo = new AccountInviteeVo();
+            vo.setUserId(invitee.getUserId());
+            vo.setNickname(invitee.getNickname());
+            vo.setAvatar(invitee.getAvatar());
+            vo.setPhone(maskPhone(invitee.getPhone()));
+            vo.setSystemLevel(invitee.getSystemLevel());
+            vo.setBindDate(formatBindDate(invitee.getReferrerBindTime()));
+            vo.setBuyAmountTotal(invitee.getBuyAmountTotal());
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    public AccountReferrerVo getReferrer(Long userId)
+    {
+        // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询)
+        if (StringUtils.isNull(userId))
+        {
+            throw new ServiceException("获取上级信息失败");
+        }
+        // STEP 2: 查询会员本人,取其 referrer_id 与绑定时间
+        AccountUser self = accountUserMapper.selectAccountUserById(userId);
+        if (StringUtils.isNull(self))
+        {
+            throw new ServiceException("获取上级信息失败");
+        }
+        AccountReferrerVo vo = new AccountReferrerVo();
+        Long referrerId = self.getReferrerId();
+        // STEP 3: 无上级(命中通用推荐码注册或历史数据)时返回空对象,前端渲染空态
+        if (StringUtils.isNull(referrerId))
+        {
+            return vo;
+        }
+        // STEP 4: 按主键查上级;上级已被逻辑删除时同样返回空对象
+        AccountUser referrer = accountUserMapper.selectAccountUserById(referrerId);
+        if (StringUtils.isNull(referrer))
+        {
+            return vo;
+        }
+        // STEP 5: 组装展示对象;绑定时间取本人记录上的 referrer_bind_time
+        vo.setUserId(referrer.getUserId());
+        vo.setNickname(referrer.getNickname());
+        vo.setAvatar(referrer.getAvatar());
+        vo.setPhone(maskPhone(referrer.getPhone()));
+        vo.setSystemLevel(referrer.getSystemLevel());
+        vo.setBindDate(formatBindDate(self.getReferrerBindTime()));
+        return vo;
+    }
+
+    /**
+     * 绑定时间格式化(yyyy-MM-dd)
+     *
+     * @param bindTime 绑定时间,可为空
+     * @return 格式化后的日期字符串;入参为空时返回 null
+     */
+    private String formatBindDate(Date bindTime)
+    {
+        return StringUtils.isNull(bindTime) ? null : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, bindTime);
+    }
+
     /**
      * 判定是否为通用推荐码
      *

--
Gitblit v1.7.1