package com.ruoyi.user.service.impl;
|
|
import java.security.SecureRandom;
|
import java.time.LocalDate;
|
import java.time.ZoneId;
|
import java.util.ArrayList;
|
import java.util.Date;
|
import java.util.List;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import com.github.pagehelper.PageInfo;
|
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.constant.ReferralConstants;
|
import com.ruoyi.user.domain.AccountProvider;
|
import com.ruoyi.user.domain.AccountUser;
|
import com.ruoyi.user.domain.dto.SubAccountCreateDto;
|
import com.ruoyi.user.domain.vo.PartnerEarningStatVo;
|
import com.ruoyi.user.domain.vo.PartnerInviteStatVo;
|
import com.ruoyi.user.domain.vo.PartnerOverviewVo;
|
import com.ruoyi.user.domain.vo.PartnerSalesStatVo;
|
import com.ruoyi.user.domain.vo.PartnerSubAccountPageVo;
|
import com.ruoyi.user.domain.vo.PartnerSubAccountStatVo;
|
import com.ruoyi.user.domain.vo.PartnerSubAccountVo;
|
import com.ruoyi.user.domain.vo.SubAccountQuotaVo;
|
import com.ruoyi.user.mapper.AccountPartnerMapper;
|
import com.ruoyi.user.mapper.AccountProviderMapper;
|
import com.ruoyi.user.mapper.AccountUserMapper;
|
import com.ruoyi.user.service.IAccountPartnerService;
|
|
/**
|
* 服务商首页概览 业务实现
|
*
|
* @author hugray
|
*/
|
@Service
|
public class AccountPartnerServiceImpl implements IAccountPartnerService
|
{
|
/** 子账号分布展示的等级(固定三段,无授权补 0) */
|
private static final String[] SUB_ACCOUNT_LEVELS = { "L1", "L2", "L3" };
|
|
/** 新邀请分布展示的等级(固定四段,C 由前端渲染为 L0) */
|
private static final String[] INVITE_LEVELS = { "C", "L1", "L2", "L3" };
|
|
/** 推荐码随机源(SecureRandom 线程安全,避免每次创建) */
|
private static final SecureRandom RANDOM = new SecureRandom();
|
|
@Autowired
|
private AccountProviderMapper accountProviderMapper;
|
|
@Autowired
|
private AccountPartnerMapper accountPartnerMapper;
|
|
@Autowired
|
private AccountUserMapper accountUserMapper;
|
|
@Override
|
public PartnerOverviewVo getOverview(Long userId)
|
{
|
// STEP 1: 校验登录态与服务商身份/资格
|
AccountProvider provider = loadProviderOrThrow(userId);
|
|
// STEP 2: 计算当月 / 上月时间边界(Java 侧算好,避免 SQL 日期方言差异)
|
LocalDate monthStartDate = LocalDate.now().withDayOfMonth(1);
|
Date monthStart = toDate(monthStartDate);
|
Date nextMonthStart = toDate(monthStartDate.plusMonths(1));
|
Date prevMonthStart = toDate(monthStartDate.minusMonths(1));
|
|
// STEP 3: 子账号总数 / 等级分布 / 当月与上月新增绑定(同源,单条 SQL)
|
PartnerSubAccountStatVo subStat = accountPartnerMapper.selectSubAccountStat(
|
userId, monthStart, nextMonthStart, prevMonthStart);
|
|
// STEP 4: 名下子账号当月与上月收益合计
|
PartnerEarningStatVo earningStat = accountPartnerMapper.selectEarningMonthStat(
|
userId, monthStart, nextMonthStart, prevMonthStart);
|
|
// STEP 5: 名下子账号当月销售额与销售单数
|
PartnerSalesStatVo salesStat = accountPartnerMapper.selectSalesMonthStat(
|
userId, monthStart, nextMonthStart);
|
|
// STEP 6: 名下子账号当月进货额
|
Long monthPurchaseAmount = accountPartnerMapper.selectPurchaseMonthAmount(
|
userId, monthStart, nextMonthStart);
|
|
// STEP 7: 当月新邀请总数与等级分布
|
PartnerInviteStatVo inviteStat = accountPartnerMapper.selectInviteMonthStat(
|
userId, monthStart, nextMonthStart);
|
|
// STEP 8: 组装出参(缺省一律补 0;环比无基数返回 null)
|
PartnerOverviewVo vo = new PartnerOverviewVo();
|
vo.setCompanyName(provider.getCompanyName());
|
vo.setSubAccount(buildSubAccountSummary(subStat));
|
vo.setPerformance(buildPerformanceSummary(earningStat, salesStat, monthPurchaseAmount, subStat, inviteStat));
|
return vo;
|
}
|
|
@Override
|
public PartnerSubAccountPageVo listSubAccounts(Long userId, String level, String keyword)
|
{
|
// STEP 1: 校验登录态与服务商身份/资格
|
loadProviderOrThrow(userId);
|
|
// STEP 2: 等级筛选归一化(仅接受 L1/L2/L3,其余值按「全部」处理)
|
String normalizedLevel = normalizeSubLevelFilter(level);
|
|
// STEP 3: 搜索关键字归一化(去空格 + 转义 like 通配符,空串按「不过滤」处理)
|
String normalizedKeyword = normalizeKeyword(keyword);
|
|
// STEP 4: 分页查询(分页 limit 由 Controller 侧 startPage() 织入,语句本身不挂 join)
|
List<PartnerSubAccountVo> list = accountPartnerMapper.selectSubAccountList(userId, normalizedLevel, normalizedKeyword);
|
|
// STEP 5: 从 PageHelper 结果取总数与分页参数,组装 {list,total,pageNum,pageSize}
|
PageInfo<PartnerSubAccountVo> pageInfo = new PageInfo<>(list);
|
PartnerSubAccountPageVo vo = new PartnerSubAccountPageVo();
|
vo.setList(list);
|
vo.setTotal(pageInfo.getTotal());
|
vo.setPageNum(pageInfo.getPageNum());
|
vo.setPageSize(pageInfo.getPageSize());
|
return vo;
|
}
|
|
@Override
|
public SubAccountQuotaVo getSubAccountQuota(Long userId)
|
{
|
// STEP 1: 校验登录态与服务商身份/资格
|
AccountProvider provider = loadProviderOrThrow(userId);
|
|
// STEP 2: 统计名下各等级已开通数
|
PartnerSubAccountStatVo used = accountPartnerMapper.selectSubAccountLevelCount(userId);
|
|
// STEP 3: 剩余额度 = 权益 − 已开通数(下限 0)
|
SubAccountQuotaVo vo = new SubAccountQuotaVo();
|
vo.setL1(calcRemainQuota(quotaOf(provider, "L1"), usedOf(used, "L1")));
|
vo.setL2(calcRemainQuota(quotaOf(provider, "L2"), usedOf(used, "L2")));
|
vo.setL3(calcRemainQuota(quotaOf(provider, "L3"), usedOf(used, "L3")));
|
return vo;
|
}
|
|
@Override
|
public Long createSubAccount(Long userId, SubAccountCreateDto dto)
|
{
|
// STEP 1: 校验登录态与服务商身份/资格
|
AccountProvider provider = loadProviderOrThrow(userId);
|
|
// STEP 2: 等级合法性校验(仅接受 L1/L2/L3)
|
String level = normalizeSubLevelFilter(dto.getLevel());
|
if (level == null)
|
{
|
throw new ServiceException("请选择等级");
|
}
|
|
// STEP 3: 手机号全局唯一性校验(子账号即手机号账号,走验证码登录)
|
String phone = dto.getPhone();
|
if (StringUtils.isNotNull(accountUserMapper.selectAccountUserByPhone(phone)))
|
{
|
throw new ServiceException("手机号已注册");
|
}
|
|
// STEP 4: 该等级剩余额度校验(权益 − 已开通数 > 0 才允许开通)
|
PartnerSubAccountStatVo used = accountPartnerMapper.selectSubAccountLevelCount(userId);
|
if (calcRemainQuota(quotaOf(provider, level), usedOf(used, level)) <= 0)
|
{
|
throw new ServiceException("该等级剩余额度不足");
|
}
|
|
// STEP 5: 组装子账号并写入(沿用 C 端注册口径:账号=手机号、无密码、状态正常、绑定供货关系)
|
AccountUser subAccount = new AccountUser();
|
subAccount.setUserName(phone);
|
subAccount.setPhone(phone);
|
subAccount.setStatus(AccountUser.STATUS_NORMAL);
|
subAccount.setSystemLevel(level);
|
subAccount.setSupplierId(userId);
|
subAccount.setSupplierBindTime(new Date());
|
subAccount.setMyReferralCode(generateUniqueReferralCode());
|
subAccount.setCreateBy(SecurityUtils.getUsername());
|
if (accountUserMapper.insertAccountUser(subAccount) <= 0)
|
{
|
throw new ServiceException("新增失败,请稍后重试");
|
}
|
|
// STEP 6: 返回新子账号会员ID
|
return subAccount.getUserId();
|
}
|
|
/**
|
* 加载服务商并校验资格(登录态为空 / 非服务商 / 资格禁用一律抛业务异常)
|
*
|
* 首页概览、子账号列表、额度、新增共用同一身份校验口径,故抽为私有方法。
|
*
|
* @param userId 服务商账号ID(来自登录态)
|
* @return 服务商配置
|
*/
|
private AccountProvider loadProviderOrThrow(Long userId)
|
{
|
if (userId == null)
|
{
|
throw new ServiceException("账号未登录,请重新登录");
|
}
|
AccountProvider provider = accountProviderMapper.selectProviderByUserId(userId);
|
if (provider == null)
|
{
|
throw new ServiceException("当前账号不是服务商");
|
}
|
if (AccountProvider.STATUS_DISABLED.equals(provider.getStatus()))
|
{
|
throw new ServiceException("服务商资格已禁用,请联系管理员");
|
}
|
return provider;
|
}
|
|
/**
|
* 等级筛选归一化:仅接受 L1/L2/L3,空值或非法值统一返回 null(表示「全部」)
|
*/
|
private String normalizeSubLevelFilter(String level)
|
{
|
if (StringUtils.isEmpty(level))
|
{
|
return null;
|
}
|
for (String subLevel : SUB_ACCOUNT_LEVELS)
|
{
|
if (subLevel.equals(level))
|
{
|
return level;
|
}
|
}
|
return null;
|
}
|
|
/**
|
* 搜索关键字归一化:去首尾空格,空串返回 null(表示不过滤)
|
*
|
* 转义 like 通配符(\ % _),保证用户输入按字面匹配,避免 % 或 _ 扩大匹配范围;
|
* 与 XML 中 escape '\' 配套使用。
|
*
|
* @param keyword 原始搜索关键字(可空)
|
* @return 转义后的关键字,空串返回 null
|
*/
|
private String normalizeKeyword(String keyword)
|
{
|
String trimmed = StringUtils.trim(keyword);
|
if (StringUtils.isEmpty(trimmed))
|
{
|
return null;
|
}
|
return trimmed.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
}
|
|
/**
|
* 取指定等级的服务商权益额度(仅 L1/L2/L3)
|
*/
|
private Integer quotaOf(AccountProvider provider, String level)
|
{
|
switch (level)
|
{
|
case "L1":
|
return provider.getL1Quota();
|
case "L2":
|
return provider.getL2Quota();
|
case "L3":
|
return provider.getL3Quota();
|
default:
|
return null;
|
}
|
}
|
|
/**
|
* 取指定等级的已开通数(仅 L1/L2/L3;统计对象为空时返回 null,由空值归零兜底)
|
*/
|
private Integer usedOf(PartnerSubAccountStatVo stat, String level)
|
{
|
if (stat == null)
|
{
|
return null;
|
}
|
switch (level)
|
{
|
case "L1":
|
return stat.getL1();
|
case "L2":
|
return stat.getL2();
|
case "L3":
|
return stat.getL3();
|
default:
|
return null;
|
}
|
}
|
|
/**
|
* 计算剩余额度:权益 − 已开通数,下限 0(任一入参为空按 0 处理)
|
*/
|
private int calcRemainQuota(Integer quota, Integer used)
|
{
|
return Math.max(nullToZero(quota) - nullToZero(used), 0);
|
}
|
|
/**
|
* 生成库内唯一的推荐码(8 位大写字母数字,冲突重试,超限抛业务异常)
|
*
|
* 与 C 端注册同口径,子账号开通时也需生成。
|
*/
|
private String generateUniqueReferralCode()
|
{
|
String chars = ReferralConstants.CODE_CHARS;
|
for (int retry = 0; retry < ReferralConstants.GEN_MAX_RETRY; retry++)
|
{
|
StringBuilder sb = new StringBuilder(ReferralConstants.CODE_LENGTH);
|
for (int i = 0; i < ReferralConstants.CODE_LENGTH; i++)
|
{
|
sb.append(chars.charAt(RANDOM.nextInt(chars.length())));
|
}
|
String code = sb.toString();
|
if (StringUtils.isNull(accountUserMapper.selectAccountUserByReferralCode(code)))
|
{
|
return code;
|
}
|
}
|
throw new ServiceException("新增失败,请稍后重试");
|
}
|
|
/**
|
* 组装卡片一「开通子账号数」:总数 + L1/L2/L3 分布(固定三段补 0)
|
*/
|
private PartnerOverviewVo.SubAccountSummary buildSubAccountSummary(PartnerSubAccountStatVo stat)
|
{
|
PartnerOverviewVo.SubAccountSummary summary = new PartnerOverviewVo.SubAccountSummary();
|
if (stat == null)
|
{
|
summary.setLevels(buildLevelList(SUB_ACCOUNT_LEVELS, 0, 0, 0));
|
return summary;
|
}
|
summary.setTotalCount(nullToZero(stat.getTotal()));
|
summary.setLevels(buildLevelList(SUB_ACCOUNT_LEVELS,
|
nullToZero(stat.getL1()), nullToZero(stat.getL2()), nullToZero(stat.getL3()), 0));
|
return summary;
|
}
|
|
/**
|
* 组装卡片二「子账号业绩合计」:双大卡 + 8 项细分指标
|
*/
|
private PartnerOverviewVo.PerformanceSummary buildPerformanceSummary(PartnerEarningStatVo earningStat,
|
PartnerSalesStatVo salesStat,
|
Long monthPurchaseAmount,
|
PartnerSubAccountStatVo subStat,
|
PartnerInviteStatVo inviteStat)
|
{
|
PartnerOverviewVo.PerformanceSummary summary = new PartnerOverviewVo.PerformanceSummary();
|
|
// 大卡一:当月合计收益 + 环比
|
long monthEarning = earningStat == null ? 0L : nullToZero(earningStat.getMonthAmount());
|
long prevMonthEarning = earningStat == null ? 0L : nullToZero(earningStat.getPrevMonthAmount());
|
summary.setMonthEarning(monthEarning);
|
summary.setMonthEarningTrend(calcGrowthRate(monthEarning, prevMonthEarning));
|
|
// 大卡二:当月新增绑定 + 环比
|
int monthNewBinding = subStat == null ? 0 : nullToZero(subStat.getMonthNew());
|
int prevMonthNewBinding = subStat == null ? 0 : nullToZero(subStat.getPrevMonthNew());
|
summary.setMonthNewBinding(monthNewBinding);
|
summary.setMonthNewBindingTrend(calcGrowthRate(monthNewBinding, prevMonthNewBinding));
|
|
// 细分:当月总计销售额 / 当月销售单 / 当月进货额
|
summary.setMonthSalesAmount(salesStat == null ? 0L : nullToZero(salesStat.getSalesAmount()));
|
summary.setMonthSalesCount(salesStat == null ? 0 : nullToZero(salesStat.getSalesCount()));
|
summary.setMonthPurchaseAmount(nullToZero(monthPurchaseAmount));
|
|
// 细分:当月新邀请总数 + 等级分布
|
summary.setMonthNewInviteTotal(inviteStat == null ? 0 : nullToZero(inviteStat.getInvitedTotal()));
|
if (inviteStat == null)
|
{
|
summary.setMonthNewLevels(buildLevelList(INVITE_LEVELS, 0, 0, 0, 0));
|
}
|
else
|
{
|
summary.setMonthNewLevels(buildLevelList(INVITE_LEVELS,
|
nullToZero(inviteStat.getL0()), nullToZero(inviteStat.getL1()),
|
nullToZero(inviteStat.getL2()), nullToZero(inviteStat.getL3())));
|
}
|
return summary;
|
}
|
|
/**
|
* 按固定等级顺序组装等级计数列表(数量按位置一一对应)
|
*
|
* @param levels 等级顺序(如 L1/L2/L3 或 C/L1/L2/L3)
|
* @param counts 与 levels 等长的数量数组
|
*/
|
private List<PartnerOverviewVo.LevelCountVo> buildLevelList(String[] levels, int... counts)
|
{
|
List<PartnerOverviewVo.LevelCountVo> list = new ArrayList<>();
|
for (int i = 0; i < levels.length; i++)
|
{
|
list.add(new PartnerOverviewVo.LevelCountVo(levels[i], i < counts.length ? counts[i] : 0));
|
}
|
return list;
|
}
|
|
/**
|
* 计算环比百分比;上月基数为 0 时返回 null(前端隐藏环比)
|
*/
|
private Integer calcGrowthRate(long current, long previous)
|
{
|
if (previous == 0L)
|
{
|
return null;
|
}
|
return (int) Math.round((current - previous) * 100.0 / previous);
|
}
|
|
/**
|
* LocalDate 转 java.util.Date(按系统时区当天零点)
|
*/
|
private Date toDate(LocalDate date)
|
{
|
return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
}
|
|
/**
|
* Long 空值归零
|
*/
|
private long nullToZero(Long value)
|
{
|
return value == null ? 0L : value;
|
}
|
|
/**
|
* Integer 空值归零
|
*/
|
private int nullToZero(Integer value)
|
{
|
return value == null ? 0 : value;
|
}
|
}
|