feat(recommendation): 实现推荐客户管理全流程功能

- 新增推荐记录、推荐人信息、推荐费结算实体及数据库表结构
- 实现推荐记录小程序端接口,包括新增报备、获取推荐码、查询我的推荐及统计
- 实现后台管理端推荐记录分页查询、确认有效、作废、结算及批量结算接口
- 实现推荐人信息查询及推荐统计汇总逻辑
- 实现推荐记录导出功能,支持导出推荐详情数据
- 完成推荐状态变更时推荐人统计数据同步更新
- 添加相关Mapper接口和Service接口及实现
- 使用MyBatis Plus分页插件支持分页查询
- 集成Spring Security权限校验和操作日志注解
- 优化推荐码生成和唯一性校验机制
This commit is contained in:
2026-04-16 16:40:08 +08:00
parent cc2fe7b172
commit 7095c4bf96
13 changed files with 1126 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
"version": 2,
"sessions": {
"8b8cdf8cb3404efeac18615f9f419cb1": [
{
"expertId": "SeniorDeveloper",
"name": "吴八哥",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1776323986694,
"industryId": "02-Engineering"
}
]
},
"lastUpdated": 1776324781452
}

View File

@@ -0,0 +1,104 @@
package com.gxwebsoft.app.recommendation.controller;
import com.gxwebsoft.app.recommendation.param.LeadReferralParam;
import com.gxwebsoft.app.recommendation.service.LeadReferralService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.annotation.OperationLog;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 推荐记录管理控制器(后台管理端)
*
* @author 科技小王子
* @since 2026-04-16
*/
@Slf4j
@RestController
@RequestMapping("/app/lead/referral/admin")
@Tag(name = "推荐客户管理", description = "后台管理端推荐记录相关接口")
public class LeadReferralAdminController extends BaseController {
@Autowired
private LeadReferralService leadReferralService;
@Operation(summary = "分页查询推荐记录")
@GetMapping("/page")
@PreAuthorize("hasAuthority('app:referral:page') or hasAuthority('app:referral:view')")
public ApiResult<Map<String, Object>> getReferralPage(LeadReferralParam param) {
return success(leadReferralService.getReferralPage(param));
}
@Operation(summary = "获取推荐统计汇总")
@GetMapping("/statistics")
@PreAuthorize("hasAuthority('app:referral:statistics')")
public ApiResult<Map<String, Object>> getStatistics(LeadReferralParam param) {
return success(leadReferralService.getStatistics(param));
}
@Operation(summary = "确认推荐有效")
@PutMapping("/confirm/{id}")
@PreAuthorize("hasAuthority('app:referral:confirm')")
@OperationLog
public ApiResult<?> confirmReferral(
@Parameter(description = "推荐ID") @PathVariable Long id) {
if (leadReferralService.confirmReferral(id)) {
return success("确认成功");
}
return fail("确认失败,状态已变更");
}
@Operation(summary = "作废推荐")
@PutMapping("/invalid/{id}")
@PreAuthorize("hasAuthority('app:referral:invalid')")
@OperationLog
public ApiResult<?> invalidateReferral(
@Parameter(description = "推荐ID") @PathVariable Long id,
@Parameter(description = "作废原因") @RequestParam(required = false) String reason) {
if (leadReferralService.invalidateReferral(id, reason)) {
return success("已作废");
}
return fail("操作失败");
}
@Operation(summary = "结算推荐费")
@PutMapping("/settle/{id}")
@PreAuthorize("hasAuthority('app:referral:settle')")
@OperationLog
public ApiResult<?> settleReferral(
@Parameter(description = "推荐ID") @PathVariable Long id,
@Parameter(description = "结算金额(为空则用默认推荐费)") @RequestParam(required = false) BigDecimal amount,
@Parameter(description = "备注") @RequestParam(required = false) String remarks) {
if (leadReferralService.settleReferral(id, amount, remarks)) {
return success("结算成功");
}
return fail("结算失败,请确认状态为「有效」");
}
@Operation(summary = "批量结算推荐费")
@PutMapping("/settle/batch")
@PreAuthorize("hasAuthority('app:referral:settle')")
@OperationLog
public ApiResult<?> batchSettleReferrals(
@Parameter(description = "推荐ID列表") @RequestBody List<Long> ids) {
int count = leadReferralService.batchSettleReferrals(ids);
return success("成功结算 " + count + "");
}
@Operation(summary = "导出推荐数据")
@GetMapping("/export")
@PreAuthorize("hasAuthority('app:referral:export')")
public ApiResult<List<Map<String, Object>>> exportReferrals(LeadReferralParam param) {
return success(leadReferralService.exportReferrals(param));
}
}

View File

@@ -0,0 +1,64 @@
package com.gxwebsoft.app.recommendation.controller;
import com.gxwebsoft.app.recommendation.param.LeadReferralParam;
import com.gxwebsoft.app.recommendation.service.LeadReferralService;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 推荐记录控制器(小程序端)
*
* @author 科技小王子
* @since 2026-04-16
*/
@Slf4j
@RestController
@RequestMapping("/app/lead/referral")
@Tag(name = "推荐客户", description = "小程序端推荐报备相关接口")
public class LeadReferralController extends BaseController {
@Autowired
private LeadReferralService leadReferralService;
@Operation(summary = "报备客户")
@PostMapping("/add")
public ApiResult<?> addReferral(@RequestBody LeadReferralParam param) {
return success(leadReferralService.addReferral(param));
}
@Operation(summary = "获取我的推荐码(无则自动生成)")
@GetMapping("/my/code")
public ApiResult<?> getMyReferralCode() {
String code = leadReferralService.getMyReferralCode();
return success(code != null ? code : "");
}
@Operation(summary = "获取我的推荐记录(分页)")
@GetMapping("/my")
public ApiResult<Map<String, Object>> getMyReferrals(LeadReferralParam param) {
return success(leadReferralService.getMyReferrals(param));
}
@Operation(summary = "获取我的推荐统计")
@GetMapping("/my/stats")
public ApiResult<Map<String, Object>> getMyStats() {
return success(leadReferralService.getMyStats());
}
@Operation(summary = "根据推荐码获取推荐人信息")
@GetMapping("/referrer/{code}")
public ApiResult<Map<String, Object>> getReferrerByCode(
@Parameter(description = "推荐码") @PathVariable String code) {
return success(leadReferralService.getReferrerByCode(code));
}
}

View File

@@ -0,0 +1,103 @@
package com.gxwebsoft.app.recommendation.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 推荐记录表
*
* @author 科技小王子
* @since 2026-04-16
*/
@Data
@TableName("app_lead_referral")
@Schema(name = "LeadReferral", description = "推荐记录")
public class LeadReferral implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "推荐码(推荐人分享给客户的码)")
private String referralCode;
@Schema(description = "推荐人用户IDNULL=匿名推荐)")
private Integer referrerId;
@Schema(description = "推荐人姓名")
private String referrerName;
@Schema(description = "推荐人手机号")
private String referrerPhone;
@Schema(description = "客户姓名")
private String customerName;
@Schema(description = "客户手机号")
private String customerPhone;
@Schema(description = "客户公司名称")
private String customerCompany;
@Schema(description = "客户需求描述")
private String requirement;
@Schema(description = "预约联系时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private LocalDateTime appointmentTime;
@Schema(description = "推荐人备注")
private String remarks;
@Schema(description = "推荐费(元)")
private BigDecimal referralFee;
@Schema(description = "推荐状态: 0=待确认, 1=有效, 2=无效, 3=已结算")
private Integer referralStatus;
@Schema(description = "作废原因")
private String invalidReason;
@Schema(description = "作废时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime invalidTime;
@Schema(description = "确认有效时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime confirmedTime;
@Schema(description = "结算时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime settledTime;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建人ID")
private Integer createBy;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新人ID")
private Integer updateBy;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "删除标记: 0=未删, 1=已删")
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,59 @@
package com.gxwebsoft.app.recommendation.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 推荐费结算记录表
*
* @author 科技小王子
* @since 2026-04-16
*/
@Data
@TableName("app_referral_settlement")
@Schema(name = "ReferralSettlement", description = "推荐费结算记录")
public class ReferralSettlement implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "关联推荐记录ID")
private Long referralId;
@Schema(description = "推荐人用户ID")
private Integer referrerId;
@Schema(description = "结算金额(元)")
private BigDecimal amount;
@Schema(description = "结算单号")
private String settlementNo;
@Schema(description = "结算备注")
private String remarks;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "结算操作人ID")
private Integer createBy;
@Schema(description = "结算时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "删除标记: 0=未删, 1=已删")
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,72 @@
package com.gxwebsoft.app.recommendation.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 推荐人信息表
*
* @author 科技小王子
* @since 2026-04-16
*/
@Data
@TableName("app_referrer_info")
@Schema(name = "ReferrerInfo", description = "推荐人信息")
public class ReferrerInfo implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "用户ID")
private Integer userId;
@Schema(description = "推荐码(唯一)")
private String referralCode;
@Schema(description = "累计推荐人数")
private Integer totalReferrals;
@Schema(description = "有效推荐数")
private Integer validReferrals;
@Schema(description = "已结算数")
private Integer settledCount;
@Schema(description = "累计获得推荐费(元)")
private BigDecimal totalEarned;
@Schema(description = "待结算金额(元)")
private BigDecimal pendingAmount;
@Schema(description = "租户ID")
private Integer tenantId;
@Schema(description = "创建人ID")
private Integer createBy;
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Schema(description = "更新人ID")
private Integer updateBy;
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Schema(description = "删除标记: 0=未删, 1=已删")
@TableLogic
private Integer deleted;
}

View File

@@ -0,0 +1,27 @@
package com.gxwebsoft.app.recommendation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gxwebsoft.app.recommendation.entity.LeadReferral;
import com.gxwebsoft.app.recommendation.param.LeadReferralParam;
import org.apache.ibatis.annotations.Param;
/**
* 推荐记录 Mapper
*
* @author 科技小王子
* @since 2026-04-16
*/
public interface LeadReferralMapper extends BaseMapper<LeadReferral> {
/**
* 分页查询推荐记录(关联推荐人信息)
*/
IPage<LeadReferral> selectReferralPage(Page<?> page, @Param("param") LeadReferralParam param);
/**
* 查询用户的所有推荐记录
*/
IPage<LeadReferral> selectByReferrerId(Page<?> page, @Param("referrerId") Integer referrerId);
}

View File

@@ -0,0 +1,13 @@
package com.gxwebsoft.app.recommendation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.recommendation.entity.ReferralSettlement;
/**
* 推荐费结算记录 Mapper
*
* @author 科技小王子
* @since 2026-04-16
*/
public interface ReferralSettlementMapper extends BaseMapper<ReferralSettlement> {
}

View File

@@ -0,0 +1,27 @@
package com.gxwebsoft.app.recommendation.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.gxwebsoft.app.recommendation.entity.ReferrerInfo;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 推荐人信息 Mapper
*
* @author 科技小王子
* @since 2026-04-16
*/
public interface ReferrerInfoMapper extends BaseMapper<ReferrerInfo> {
/**
* 根据用户ID查询推荐人信息
*/
@Select("SELECT * FROM app_referrer_info WHERE user_id = #{userId} AND deleted = 0 LIMIT 1")
ReferrerInfo selectByUserId(@Param("userId") Integer userId);
/**
* 根据推荐码查询推荐人信息
*/
@Select("SELECT * FROM app_referrer_info WHERE referral_code = #{referralCode} AND deleted = 0 LIMIT 1")
ReferrerInfo selectByReferralCode(@Param("referralCode") String referralCode);
}

View File

@@ -0,0 +1,53 @@
package com.gxwebsoft.app.recommendation.param;
import com.gxwebsoft.common.core.web.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 推荐记录查询参数
*
* @author 科技小王子
* @since 2026-04-16
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(name = "LeadReferralParam", description = "推荐记录查询参数")
public class LeadReferralParam extends PageParam {
@Schema(description = "推荐状态: 0=待确认, 1=有效, 2=无效, 3=已结算")
private Integer referralStatus;
@Schema(description = "推荐人用户ID")
private Integer referrerId;
@Schema(description = "客户姓名(模糊)")
private String customerName;
@Schema(description = "客户手机号")
private String customerPhone;
@Schema(description = "客户公司")
private String customerCompany;
@Schema(description = "推荐需求")
private String requirement;
@Schema(description = "预约时间")
private LocalDateTime appointmentTime;
@Schema(description = "备注")
private String remarks;
@Schema(description = "推荐码")
private String referralCode;
@Schema(description = "开始日期")
private String startDate;
@Schema(description = "结束日期")
private String endDate;
}

View File

@@ -0,0 +1,83 @@
package com.gxwebsoft.app.recommendation.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.app.recommendation.entity.LeadReferral;
import com.gxwebsoft.app.recommendation.entity.ReferralSettlement;
import com.gxwebsoft.app.recommendation.param.LeadReferralParam;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 推荐记录 Service
*
* @author 科技小王子
* @since 2026-04-16
*/
public interface LeadReferralService extends IService<LeadReferral> {
// ========== 小程序端 API ==========
/**
* 用户报备客户
*/
LeadReferral addReferral(LeadReferralParam param);
/**
* 获取我的推荐码(无则生成)
*/
String getMyReferralCode();
/**
* 获取我的推荐记录(分页)
*/
Map<String, Object> getMyReferrals(LeadReferralParam param);
/**
* 获取我的推荐统计
*/
Map<String, Object> getMyStats();
// ========== 后台管理 API ==========
/**
* 分页查询推荐记录
*/
Map<String, Object> getReferralPage(LeadReferralParam param);
/**
* 确认推荐有效
*/
boolean confirmReferral(Long id);
/**
* 作废推荐
*/
boolean invalidateReferral(Long id, String reason);
/**
* 结算推荐费
*/
boolean settleReferral(Long id, BigDecimal amount, String remarks);
/**
* 批量结算推荐费
*/
int batchSettleReferrals(List<Long> ids);
/**
* 根据推荐码获取推荐人信息
*/
Map<String, Object> getReferrerByCode(String code);
/**
* 获取推荐统计汇总
*/
Map<String, Object> getStatistics(LeadReferralParam param);
/**
* 导出推荐数据
*/
List<Map<String, Object>> exportReferrals(LeadReferralParam param);
}

View File

@@ -0,0 +1,423 @@
package com.gxwebsoft.app.recommendation.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.app.recommendation.mapper.ReferrerInfoMapper;
import com.gxwebsoft.app.recommendation.entity.LeadReferral;
import com.gxwebsoft.app.recommendation.entity.ReferralSettlement;
import com.gxwebsoft.app.recommendation.entity.ReferrerInfo;
import com.gxwebsoft.app.recommendation.mapper.LeadReferralMapper;
import com.gxwebsoft.app.recommendation.mapper.ReferralSettlementMapper;
import com.gxwebsoft.app.recommendation.param.LeadReferralParam;
import com.gxwebsoft.app.recommendation.service.LeadReferralService;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 推荐记录 Service 实现
*
* @author 科技小王子
* @since 2026-04-16
*/
@Slf4j
@Service
public class LeadReferralServiceImpl extends ServiceImpl<LeadReferralMapper, LeadReferral>
implements LeadReferralService {
@Autowired
private LeadReferralMapper leadReferralMapper;
@Autowired
private ReferrerInfoMapper referrerInfoMapper;
@Autowired
private ReferralSettlementMapper settlementMapper;
@Autowired
private UserService userService;
/** 默认推荐费(元),可配置化 */
private static final BigDecimal DEFAULT_REFERRAL_FEE = new BigDecimal("100.00");
// ========== 私有方法 ==========
private User getLoginUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof User) {
return (User) principal;
}
return null;
}
private Integer getTenantId() {
User loginUser = getLoginUser();
if (loginUser != null) {
return loginUser.getTenantId();
}
return null;
}
private String generateReferralCode() {
return "RC" + IdUtil.fastSimpleUUID().substring(0, 8).toUpperCase();
}
private String getOrCreateReferralCode(Integer userId, String userName, String phone, Integer tenantId) {
ReferrerInfo info = referrerInfoMapper.selectByUserId(userId);
if (info != null && StrUtil.isNotBlank(info.getReferralCode())) {
return info.getReferralCode();
}
// 创建新记录
String code;
do {
code = generateReferralCode();
} while (referrerInfoMapper.selectByReferralCode(code) != null);
info = new ReferrerInfo();
info.setUserId(userId);
info.setReferralCode(code);
info.setTotalReferrals(0);
info.setValidReferrals(0);
info.setSettledCount(0);
info.setTotalEarned(BigDecimal.ZERO);
info.setPendingAmount(BigDecimal.ZERO);
info.setTenantId(tenantId);
referrerInfoMapper.insert(info);
return code;
}
private Map<String, Object> buildPageResult(IPage<LeadReferral> page) {
Map<String, Object> result = new HashMap<>();
result.put("list", page.getRecords());
result.put("total", page.getTotal());
result.put("pageNum", page.getCurrent());
result.put("pageSize", page.getSize());
result.put("pages", page.getPages());
return result;
}
// ========== 小程序端 API ==========
@Override
@Transactional(rollbackFor = Exception.class)
public LeadReferral addReferral(LeadReferralParam param) {
User loginUser = getLoginUser();
Integer tenantId = getTenantId();
LeadReferral referral = new LeadReferral();
referral.setCustomerName(param.getCustomerName());
referral.setCustomerPhone(param.getCustomerPhone());
referral.setCustomerCompany(param.getCustomerCompany());
referral.setRequirement(param.getRequirement());
referral.setAppointmentTime(param.getAppointmentTime());
referral.setRemarks(param.getRemarks());
referral.setReferralFee(DEFAULT_REFERRAL_FEE);
referral.setReferralStatus(0); // 待确认
referral.setTenantId(tenantId);
referral.setCreateBy(loginUser != null ? loginUser.getUserId() : null);
referral.setCreateTime(LocalDateTime.now());
if (loginUser != null) {
referral.setReferrerId(loginUser.getUserId());
referral.setReferrerName(loginUser.getUsername());
referral.setReferrerPhone(loginUser.getPhone());
// 生成推荐码
String code = getOrCreateReferralCode(
loginUser.getUserId(),
loginUser.getUsername(),
loginUser.getPhone(),
tenantId
);
referral.setReferralCode(code);
}
baseMapper.insert(referral);
// 更新推荐人统计
if (referral.getReferrerId() != null) {
ReferrerInfo info = referrerInfoMapper.selectByUserId(referral.getReferrerId());
if (info != null) {
info.setTotalReferrals(info.getTotalReferrals() + 1);
referrerInfoMapper.updateById(info);
}
}
return referral;
}
@Override
public String getMyReferralCode() {
User loginUser = getLoginUser();
if (loginUser == null) {
return null;
}
Integer tenantId = getTenantId();
return getOrCreateReferralCode(
loginUser.getUserId(),
loginUser.getUsername(),
loginUser.getPhone(),
tenantId
);
}
@Override
public Map<String, Object> getMyReferrals(LeadReferralParam param) {
User loginUser = getLoginUser();
if (loginUser == null) {
return Collections.emptyMap();
}
Page<LeadReferral> page = new Page<>(
param.getCurrent() != null ? param.getCurrent() : 1,
param.getSize() != null ? param.getSize() : 10
);
I
return buildPageResult(result);
}
@Override
public Map<String, Object> getMyStats() {
User loginUser = getLoginUser();
Map<String, Object> stats = new HashMap<>();
if (loginUser == null) {
stats.put("totalCount", 0);
stats.put("pendingCount", 0);
stats.put("validCount", 0);
stats.put("settledCount", 0);
stats.put("pendingAmount", "0.00");
return stats;
}
ReferrerInfo info = referrerInfoMapper.selectByUserId(loginUser.getUserId());
if (info == null) {
stats.put("totalCount", 0);
stats.put("pendingCount", 0);
stats.put("validCount", 0);
stats.put("settledCount", 0);
stats.put("pendingAmount", "0.00");
} else {
stats.put("totalCount", info.getTotalReferrals());
stats.put("validCount", info.getValidReferrals());
stats.put("settledCount", info.getSettledCount());
stats.put("pendingAmount", info.getPendingAmount().toString());
// 待确认 = 总数 - 有效 - 已结算 - 无效
int invalidCount = info.getTotalReferrals() - info.getValidReferrals()
- info.getSettledCount();
LambdaQueryWrapper<LeadReferral> qw = new LambdaQueryWrapper<>();
qw.eq(LeadReferral::getReferrerId, loginUser.getUserId())
.eq(LeadReferral::getReferralStatus, 0);
stats.put("pendingCount", baseMapper.selectCount(qw));
}
// 加上推荐码
stats.put("referralCode", getMyReferralCode());
return stats;
}
// ========== 后台管理 API ==========
@Override
public Map<String, Object> getReferralPage(LeadReferralParam param) {
Page<LeadReferral> page = new Page<>(
param.getPageNum() != null ? param.getPageNum() : 1,
param.getPageSize() != null ? param.getPageSize() : 10
);
IPage<LeadReferral> result = leadReferralMapper.selectReferralPage(page, param);
return buildPageResult(result);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean confirmReferral(Long id) {
LeadReferral referral = baseMapper.selectById(id);
if (referral == null || referral.getReferralStatus() != 0) {
return false;
}
referral.setReferralStatus(1); // 有效
referral.setConfirmedTime(LocalDateTime.now());
referral.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(referral);
// 更新推荐人统计
if (referral.getReferrerId() != null) {
ReferrerInfo info = referrerInfoMapper.selectByUserId(referral.getReferrerId());
if (info != null) {
info.setValidReferrals(info.getValidReferrals() + 1);
info.setPendingAmount(info.getPendingAmount().add(referral.getReferralFee()));
referrerInfoMapper.updateById(info);
}
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean invalidateReferral(Long id, String reason) {
LeadReferral referral = baseMapper.selectById(id);
if (referral == null || referral.getReferralStatus() == 2
|| referral.getReferralStatus() == 3) {
return false;
}
referral.setReferralStatus(2); // 无效
referral.setInvalidReason(reason);
referral.setInvalidTime(LocalDateTime.now());
referral.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(referral);
// 若之前是有效状态,需回退推荐人统计
if (referral.getReferralStatus() == 1 && referral.getReferrerId() != null) {
ReferrerInfo info = referrerInfoMapper.selectByUserId(referral.getReferrerId());
if (info != null) {
info.setValidReferrals(Math.max(0, info.getValidReferrals() - 1));
info.setPendingAmount(info.getPendingAmount().subtract(referral.getReferralFee()));
referrerInfoMapper.updateById(info);
}
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean settleReferral(Long id, BigDecimal amount, String remarks) {
LeadReferral referral = baseMapper.selectById(id);
if (referral == null || referral.getReferralStatus() != 1) {
return false;
}
BigDecimal settleAmount = amount != null ? amount : referral.getReferralFee();
referral.setReferralStatus(3); // 已结算
referral.setSettledTime(LocalDateTime.now());
referral.setUpdateTime(LocalDateTime.now());
baseMapper.updateById(referral);
// 写入结算记录
ReferralSettlement settlement = new ReferralSettlement();
settlement.setReferralId(id);
settlement.setReferrerId(referral.getReferrerId());
settlement.setAmount(settleAmount);
settlement.setSettlementNo("ST" + System.currentTimeMillis());
settlement.setRemarks(remarks);
settlement.setTenantId(referral.getTenantId());
settlement.setCreateBy(getLoginUser() != null ? getLoginUser().getUserId() : null);
settlement.setCreateTime(LocalDateTime.now());
settlementMapper.insert(settlement);
// 更新推荐人统计
if (referral.getReferrerId() != null) {
ReferrerInfo info = referrerInfoMapper.selectByUserId(referral.getReferrerId());
if (info != null) {
info.setSettledCount(info.getSettledCount() + 1);
info.setPendingAmount(info.getPendingAmount().subtract(settleAmount));
info.setTotalEarned(info.getTotalEarned().add(settleAmount));
referrerInfoMapper.updateById(info);
}
}
return true;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int batchSettleReferrals(List<Long> ids) {
int count = 0;
for (Long id : ids) {
if (settleReferral(id, null, null)) {
count++;
}
}
return count;
}
@Override
public Map<String, Object> getReferrerByCode(String code) {
ReferrerInfo info = referrerInfoMapper.selectByReferralCode(code);
if (info == null) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
result.put("referrerId", info.getUserId());
result.put("referrerName", "");
result.put("referralCode", info.getReferralCode());
if (info.getUserId() != null) {
User user = userService.getByIdRel(info.getUserId());
if (user != null) {
result.put("referrerName", user.getUsername());
}
}
return result;
}
@Override
public Map<String, Object> getStatistics(LeadReferralParam param) {
Map<String, Object> stats = new HashMap<>();
LambdaQueryWrapper<LeadReferral> qw = new LambdaQueryWrapper<>();
if (param.getStartDate() != null) {
qw.ge(LeadReferral::getCreateTime,
LocalDateTime.parse(param.getStartDate() + " 00:00:00",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
if (param.getEndDate() != null) {
qw.le(LeadReferral::getCreateTime,
LocalDateTime.parse(param.getEndDate() + " 23:59:59",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
stats.put("totalCount", baseMapper.selectCount(qw.clone()));
qw.eq(LeadReferral::getReferralStatus, 0);
stats.put("pendingCount", baseMapper.selectCount(qw.clone()));
qw.eq(LeadReferral::getReferralStatus, 1);
stats.put("validCount", baseMapper.selectCount(qw.clone()));
qw.eq(LeadReferral::getReferralStatus, 3);
stats.put("settledCount", baseMapper.selectCount(qw));
return stats;
}
@Override
public List<Map<String, Object>> exportReferrals(LeadReferralParam param) {
Page<LeadReferral> page = new Page<>(1, 10000);
IPage<LeadReferral> result = leadReferralMapper.selectReferralPage(page, param);
return result.getRecords().stream().map(r -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("推荐码", r.getReferralCode());
map.put("推荐人", r.getReferrerName());
map.put("推荐人手机", r.getReferrerPhone());
map.put("客户姓名", r.getCustomerName());
map.put("客户电话", r.getCustomerPhone());
map.put("客户公司", r.getCustomerCompany());
map.put("推荐费", r.getReferralFee());
map.put("状态", getStatusText(r.getReferralStatus()));
map.put("推荐时间", r.getCreateTime());
return map;
}).collect(Collectors.toList());
}
private String getStatusText(Integer status) {
if (status == null) return "";
switch (status) {
case 0: return "待确认";
case 1: return "有效";
case 2: return "无效";
case 3: return "已结算";
default: return "";
}
}
}

View File

@@ -0,0 +1,81 @@
-- ============================================================
-- 推荐客户模块 SQL
-- 包: app
-- 创建时间: 2026-04-16
-- ============================================================
-- 1. 推荐记录表
CREATE TABLE `app_lead_referral` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`referral_code` VARCHAR(32) DEFAULT NULL COMMENT '推荐码(推荐人分享给客户的码)',
`referrer_id` INT DEFAULT NULL COMMENT '推荐人用户IDNULL=匿名推荐)',
`referrer_name` VARCHAR(64) DEFAULT NULL COMMENT '推荐人姓名',
`referrer_phone` VARCHAR(20) DEFAULT NULL COMMENT '推荐人手机号',
`customer_name` VARCHAR(64) NOT NULL COMMENT '客户姓名',
`customer_phone` VARCHAR(20) NOT NULL COMMENT '客户手机号',
`customer_company` VARCHAR(255) DEFAULT NULL COMMENT '客户公司名称',
`requirement` TEXT DEFAULT NULL COMMENT '客户需求描述',
`appointment_time` DATETIME DEFAULT NULL COMMENT '预约联系时间',
`remarks` VARCHAR(500) DEFAULT NULL COMMENT '推荐人备注',
`referral_fee` DECIMAL(10,2) DEFAULT '0.00' COMMENT '推荐费(元)',
`referral_status` TINYINT NOT NULL DEFAULT '0' COMMENT '推荐状态: 0=待确认, 1=有效, 2=无效/作废, 3=已结算',
`invalid_reason` VARCHAR(255) DEFAULT NULL COMMENT '作废原因',
`invalid_time` DATETIME DEFAULT NULL COMMENT '作废时间',
`confirmed_time` DATETIME DEFAULT NULL COMMENT '确认有效时间',
`settled_time` DATETIME DEFAULT NULL COMMENT '结算时间',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`create_by` INT DEFAULT NULL COMMENT '创建人ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` INT DEFAULT NULL COMMENT '更新人ID',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT '0' COMMENT '删除标记: 0=未删, 1=已删',
PRIMARY KEY (`id`),
KEY `idx_referrer_id` (`referrer_id`),
KEY `idx_customer_phone` (`customer_phone`),
KEY `idx_referral_status` (`referral_status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推荐记录表';
-- 2. 推荐人信息表
CREATE TABLE `app_referrer_info` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`referral_code` VARCHAR(32) NOT NULL COMMENT '推荐码(唯一)',
`total_referrals` INT NOT NULL DEFAULT '0' COMMENT '累计推荐人数',
`valid_referrals` INT NOT NULL DEFAULT '0' COMMENT '有效推荐数',
`settled_count` INT NOT NULL DEFAULT '0' COMMENT '已结算数',
`total_earned` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '累计获得推荐费(元)',
`pending_amount` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '待结算金额(元)',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`create_by` INT DEFAULT NULL COMMENT '创建人ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` INT DEFAULT NULL COMMENT '更新人ID',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT '0' COMMENT '删除标记: 0=未删, 1=已删',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_id` (`user_id`),
UNIQUE KEY `uk_referral_code` (`referral_code`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推荐人信息表';
-- 3. 推荐费结算记录表
CREATE TABLE `app_referral_settlement` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`referral_id` BIGINT NOT NULL COMMENT '关联推荐记录ID',
`referrer_id` INT NOT NULL COMMENT '推荐人用户ID',
`amount` DECIMAL(10,2) NOT NULL COMMENT '结算金额(元)',
`settlement_no` VARCHAR(32) DEFAULT NULL COMMENT '结算单号',
`remarks` VARCHAR(500) DEFAULT NULL COMMENT '结算备注',
`tenant_id` INT DEFAULT NULL COMMENT '租户ID',
`create_by` INT DEFAULT NULL COMMENT '结算操作人ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '结算时间',
`deleted` TINYINT NOT NULL DEFAULT '0' COMMENT '删除标记: 0=未删, 1=已删',
PRIMARY KEY (`id`),
KEY `idx_referral_id` (`referral_id`),
KEY `idx_referrer_id` (`referrer_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推荐费结算记录表';