trade: 分销业务 - 提现申请

This commit is contained in:
owen 2023-09-21 23:56:34 +08:00
parent 5ed99a9105
commit a7de955926
20 changed files with 216 additions and 46 deletions

View File

@ -1,25 +1,29 @@
-- 增加配置表 -- 增加配置表
create table trade_config create table trade_config
( (
id bigint auto_increment comment '自增主键' primary key, id bigint auto_increment comment '自增主键' primary key,
brokerage_enabled bit default 1 not null comment '是否启用分佣', brokerage_enabled bit default 1 not null comment '是否启用分佣',
brokerage_enabled_condition tinyint default 0 not null comment '分佣模式1-人人分销 2-指定分销', brokerage_enabled_condition tinyint default 0 not null comment '分佣模式1-人人分销 2-指定分销',
brokerage_bind_mode tinyint default 0 not null comment '分销关系绑定模式: 1-没有推广人2-新用户, 3-扫码覆盖', brokerage_bind_mode tinyint default 0 not null comment '分销关系绑定模式: 1-没有推广人2-新用户, 3-扫码覆盖',
brokerage_post_urls varchar(2000) default '' null comment '分销海报图地址数组', brokerage_post_urls varchar(2000) default '' null comment '分销海报图地址数组',
brokerage_first_percent int default 0 not null comment '一级返佣比例', brokerage_first_percent int default 0 not null comment '一级返佣比例',
brokerage_second_percent int default 0 not null comment '二级返佣比例', brokerage_second_percent int default 0 not null comment '二级返佣比例',
brokerage_withdraw_min_price int default 0 not null comment '用户提现最低金额', brokerage_withdraw_min_price int default 0 not null comment '用户提现最低金额',
brokerage_bank_names varchar(200) default '' not null comment '提现银行字典类型=brokerage_bank_name', brokerage_withdraw_fee_percent int default 0 not null comment '提现手续费百分比',
brokerage_frozen_days int default 7 not null comment '佣金冻结时间()', brokerage_bank_names varchar(200) default '' not null comment '提现银行字典类型=brokerage_bank_name',
brokerage_withdraw_type varchar(32) default '1,2,3,4' not null comment '提现方式1-钱包2-银行卡3-微信4-支付宝', brokerage_frozen_days int default 7 not null comment '佣金冻结时间()',
creator varchar(64) default '' null comment '创建者', brokerage_withdraw_type varchar(32) default '1,2,3,4' not null comment '提现方式1-钱包2-银行卡3-微信4-支付宝',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间', creator varchar(64) default '' null comment '创建者',
updater varchar(64) default '' null comment '更新者', create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', updater varchar(64) default '' null comment '更新者',
deleted bit default b'0' not null comment '是否删除', update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
tenant_id bigint default 0 not null comment '租户编号' deleted bit default b'0' not null comment '是否删除',
tenant_id bigint default 0 not null comment '租户编号'
) comment '交易中心配置'; ) comment '交易中心配置';
# alter table trade_config
# add brokerage_withdraw_fee_percent int default 0 not null comment '提现手续费百分比' after brokerage_withdraw_min_price;
# alter table trade_brokerage_user # alter table trade_brokerage_user
# add level int not null default 1 comment '等级' after frozen_price; # add level int not null default 1 comment '等级' after frozen_price;
# alter table trade_brokerage_user # alter table trade_brokerage_user
@ -83,7 +87,7 @@ create index idx_status on trade_brokerage_record (status) comment '状态';
create table trade_brokerage_withdraw create table trade_brokerage_withdraw
( (
id int auto_increment comment '编号' id bigint auto_increment comment '编号'
primary key, primary key,
user_id bigint not null comment '用户编号', user_id bigint not null comment '用户编号',
price int default 0 not null comment '提现金额', price int default 0 not null comment '提现金额',
@ -137,7 +141,8 @@ insert into system_dict_type(type, name)
values ('brokerage_record_biz_type', '佣金记录业务类型'); values ('brokerage_record_biz_type', '佣金记录业务类型');
insert into system_dict_data(dict_type, label, value, sort) insert into system_dict_data(dict_type, label, value, sort)
values ('brokerage_record_biz_type', '订单返佣', 1, 1), values ('brokerage_record_biz_type', '订单返佣', 1, 1),
('brokerage_record_biz_type', '申请提现', 2, 2); ('brokerage_record_biz_type', '申请提现', 2, 2),
('brokerage_record_biz_type', '申请提现驳回', 3, 3);
insert into system_dict_type(type, name) insert into system_dict_type(type, name)
values ('brokerage_record_status', '佣金记录状态'); values ('brokerage_record_status', '佣金记录状态');

View File

@ -93,5 +93,7 @@ public interface ErrorCodeConstants {
// ========== 分销提现 模块 1011008000 ========== // ========== 分销提现 模块 1011008000 ==========
ErrorCode BROKERAGE_WITHDRAW_NOT_EXISTS = new ErrorCode(1011008000, "佣金提现记录不存在"); ErrorCode BROKERAGE_WITHDRAW_NOT_EXISTS = new ErrorCode(1011008000, "佣金提现记录不存在");
ErrorCode BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING = new ErrorCode(1011008001, "佣金提现记录状态不是审核中"); ErrorCode BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING = new ErrorCode(1011008001, "佣金提现记录状态不是审核中");
ErrorCode BROKERAGE_WITHDRAW_MIN_PRICE = new ErrorCode(1011008002, "提现金额不能低于{}元");
ErrorCode BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH = new ErrorCode(1011008003, "您当前最多可提现{}元");
} }

View File

@ -17,6 +17,7 @@ public enum BrokerageRecordBizTypeEnum implements IntArrayValuable {
ORDER(1, "获得推广佣金", "获得推广佣金 {}", true), ORDER(1, "获得推广佣金", "获得推广佣金 {}", true),
WITHDRAW(2, "提现申请", "提现申请扣除佣金 {}", false), WITHDRAW(2, "提现申请", "提现申请扣除佣金 {}", false),
WITHDRAW_REJECT(3, "提现申请驳回", "提现申请驳回返还佣金 {}", true),
; ;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BrokerageRecordBizTypeEnum::getType).toArray(); public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BrokerageRecordBizTypeEnum::getType).toArray();

View File

@ -38,7 +38,7 @@ public class AppBrokerageUserController {
public CommonResult<AppBrokerageUserRespVO> getBrokerageUser() { public CommonResult<AppBrokerageUserRespVO> getBrokerageUser() {
AppBrokerageUserRespVO respVO = new AppBrokerageUserRespVO() AppBrokerageUserRespVO respVO = new AppBrokerageUserRespVO()
.setBrokerageEnabled(true) .setBrokerageEnabled(true)
.setPrice(2000) .setBrokeragePrice(2000)
.setFrozenPrice(3000); .setFrozenPrice(3000);
return success(respVO); return success(respVO);
} }

View File

@ -5,16 +5,19 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated; import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO; import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO; import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawRespVO;
import cn.iocoder.yudao.module.trade.service.brokerage.BrokerageWithdrawService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@Tag(name = "用户 APP - 分销提现") @Tag(name = "用户 APP - 分销提现")
@ -23,6 +26,8 @@ import static java.util.Arrays.asList;
@Validated @Validated
@Slf4j @Slf4j
public class AppBrokerageWithdrawController { public class AppBrokerageWithdrawController {
@Resource
private BrokerageWithdrawService brokerageWithdrawService;
// TODO 芋艿临时 mock => // TODO 芋艿临时 mock =>
@GetMapping("/page") @GetMapping("/page")
@ -36,12 +41,11 @@ public class AppBrokerageWithdrawController {
return success(new PageResult<>(asList(vo1, vo2), 10L)); return success(new PageResult<>(asList(vo1, vo2), 10L));
} }
// TODO 芋艿临时 mock =>
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "创建分销提现") @Operation(summary = "创建分销提现")
@PreAuthenticated @PreAuthenticated
public CommonResult<Long> createBrokerageWithdraw(@RequestBody @Valid AppBrokerageWithdrawCreateReqVO createReqVO) { public CommonResult<Long> createBrokerageWithdraw(@RequestBody @Valid AppBrokerageWithdrawCreateReqVO createReqVO) {
return success(1L); return success(brokerageWithdrawService.createBrokerageWithdraw(createReqVO, getLoginUserId()));
} }
} }

View File

@ -11,7 +11,7 @@ public class AppBrokerageUserRespVO {
private Boolean brokerageEnabled; private Boolean brokerageEnabled;
@Schema(description = "可用的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2408") @Schema(description = "可用的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "2408")
private Integer price; private Integer brokeragePrice;
@Schema(description = "冻结的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "234") @Schema(description = "冻结的佣金,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "234")
private Integer frozenPrice; private Integer frozenPrice;

View File

@ -8,8 +8,9 @@ import lombok.Data;
import org.hibernate.validator.constraints.URL; import org.hibernate.validator.constraints.URL;
import javax.validation.Validator; import javax.validation.Validator;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
@Schema(description = "用户 App - 分销提现创建 Request VO") @Schema(description = "用户 App - 分销提现创建 Request VO")
@Data @Data
@ -20,7 +21,8 @@ public class AppBrokerageWithdrawCreateReqVO {
private Integer type; private Integer type;
@Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") @Schema(description = "提现金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
@Min(value = 1, message = "提现金额不能小于 1") @PositiveOrZero(message = "提现金额不能小于 0")
@NotNull(message = "提现金额不能为空")
private Integer price; private Integer price;
// ========== 银行卡微信支付宝 提现相关字段 ========== // ========== 银行卡微信支付宝 提现相关字段 ==========

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.trade.convert.brokerage; package cn.iocoder.yudao.module.trade.convert.brokerage;
import cn.hutool.core.math.Money;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -45,7 +46,7 @@ public interface BrokerageRecordConvert {
.setBizType(bizType.getType()).setBizId(bizId) .setBizType(bizType.getType()).setBizId(bizId)
.setPrice(brokeragePrice).setTotalPrice(user.getBrokeragePrice()) .setPrice(brokeragePrice).setTotalPrice(user.getBrokeragePrice())
.setTitle(title) .setTitle(title)
.setDescription(StrUtil.format(bizType.getDescription(), String.format("¥%.2f", brokeragePrice / 100d))) .setDescription(StrUtil.format(bizType.getDescription(), new Money(0, Math.abs(brokeragePrice))))
.setStatus(status).setFrozenDays(brokerageFrozenDays).setUnfreezeTime(unfreezeTime) .setStatus(status).setFrozenDays(brokerageFrozenDays).setUnfreezeTime(unfreezeTime)
.setSourceUserLevel(sourceUserLevel).setSourceUserId(sourceUserId); .setSourceUserLevel(sourceUserLevel).setSourceUserId(sourceUserId);
} }

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.trade.convert.brokerage;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO; import cn.iocoder.yudao.module.member.api.user.dto.MemberUserRespDTO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRejectReqVO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawRespVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@ -22,7 +22,7 @@ public interface BrokerageWithdrawConvert {
BrokerageWithdrawConvert INSTANCE = Mappers.getMapper(BrokerageWithdrawConvert.class); BrokerageWithdrawConvert INSTANCE = Mappers.getMapper(BrokerageWithdrawConvert.class);
BrokerageWithdrawDO convert(BrokerageWithdrawRejectReqVO bean); BrokerageWithdrawDO convert(AppBrokerageWithdrawCreateReqVO createReqVO, Long userId, Integer feePrice);
BrokerageWithdrawRespVO convert(BrokerageWithdrawDO bean); BrokerageWithdrawRespVO convert(BrokerageWithdrawDO bean);

View File

@ -29,7 +29,7 @@ public class BrokerageWithdrawDO extends BaseDO {
* 编号 * 编号
*/ */
@TableId @TableId
private Integer id; private Long id;
/** /**
* 用户编号 * 用户编号
* *

View File

@ -81,6 +81,10 @@ public class TradeConfigDO extends BaseDO {
* 用户提现最低金额 * 用户提现最低金额
*/ */
private Integer brokerageWithdrawMinPrice; private Integer brokerageWithdrawMinPrice;
/**
* 用户提现手续费百分比
*/
private Integer brokerageWithdrawFeePercent;
/** /**
* 提现银行 * 提现银行
*/ */

View File

@ -38,7 +38,7 @@ public interface BrokerageUserMapper extends BaseMapperX<BrokerageUserDO> {
default void updatePriceIncr(Long id, Integer incrCount) { default void updatePriceIncr(Long id, Integer incrCount) {
Assert.isTrue(incrCount > 0); Assert.isTrue(incrCount > 0);
LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>() LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>()
.setSql(" price = price + " + incrCount) .setSql(" brokerage_price = brokerage_price + " + incrCount)
.eq(BrokerageUserDO::getId, id); .eq(BrokerageUserDO::getId, id);
update(null, lambdaUpdateWrapper); update(null, lambdaUpdateWrapper);
} }
@ -49,13 +49,14 @@ public interface BrokerageUserMapper extends BaseMapperX<BrokerageUserDO> {
* *
* @param id 用户编号 * @param id 用户编号
* @param incrCount 增加佣金负数 * @param incrCount 增加佣金负数
* @return 更新行数
*/ */
default void updatePriceDecr(Long id, Integer incrCount) { default int updatePriceDecr(Long id, Integer incrCount) {
Assert.isTrue(incrCount < 0); Assert.isTrue(incrCount < 0);
LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>() LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>()
.setSql(" price = price + " + incrCount) // 负数所以使用 + .setSql(" brokerage_price = brokerage_price + " + incrCount) // 负数所以使用 +
.eq(BrokerageUserDO::getId, id); .eq(BrokerageUserDO::getId, id);
update(null, lambdaUpdateWrapper); return update(null, lambdaUpdateWrapper);
} }
/** /**
@ -98,7 +99,7 @@ public interface BrokerageUserMapper extends BaseMapperX<BrokerageUserDO> {
Assert.isTrue(incrCount < 0); Assert.isTrue(incrCount < 0);
LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>() LambdaUpdateWrapper<BrokerageUserDO> lambdaUpdateWrapper = new LambdaUpdateWrapper<BrokerageUserDO>()
.setSql(" frozen_price = frozen_price + " + incrCount + // 负数所以使用 + .setSql(" frozen_price = frozen_price + " + incrCount + // 负数所以使用 +
", price = price + " + -incrCount) // 负数所以使用 - ", brokerage_price = brokerage_price + " + -incrCount) // 负数所以使用 -
.eq(BrokerageUserDO::getId, id) .eq(BrokerageUserDO::getId, id)
.ge(BrokerageUserDO::getFrozenPrice, -incrCount); // cas 逻辑 .ge(BrokerageUserDO::getFrozenPrice, -incrCount); // cas 逻辑
return update(null, lambdaUpdateWrapper); return update(null, lambdaUpdateWrapper);

View File

@ -42,6 +42,17 @@ public interface BrokerageRecordService {
*/ */
void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, @Valid List<BrokerageAddReqBO> list); void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, @Valid List<BrokerageAddReqBO> list);
/**
* 增加佣金
*
* @param userId 会员编号
* @param bizType 业务类型
* @param bizId 业务编号
* @param brokeragePrice 佣金
* @param title 标题
*/
void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, int brokeragePrice, String title);
/** /**
* 取消佣金将佣金记录状态修改为已失效 * 取消佣金将佣金记录状态修改为已失效
* *

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.service.brokerage; package cn.iocoder.yudao.module.trade.service.brokerage;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.math.Money;
import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -28,6 +29,10 @@ import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH;
/** /**
* 佣金记录 Service 实现类 * 佣金记录 Service 实现类
@ -217,6 +222,29 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
return summaryBO != null ? summaryBO : new UserBrokerageSummaryBO(0, 0); return summaryBO != null ? summaryBO : new UserBrokerageSummaryBO(0, 0);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public void addBrokerage(Long userId, BrokerageRecordBizTypeEnum bizType, String bizId, int brokeragePrice, String title) {
// 校验佣金余额
BrokerageUserDO user = brokerageUserService.getBrokerageUser(userId);
int balance = Optional.of(user)
.map(BrokerageUserDO::getBrokeragePrice).orElse(0);
if (balance < brokeragePrice) {
throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, new Money(0, balance));
}
// 扣减佣金余额
boolean success = brokerageUserService.updateUserPrice(userId, brokeragePrice);
if (!success) {
throw exception(BROKERAGE_WITHDRAW_USER_BALANCE_NOT_ENOUGH, new Money(0, balance));
}
// 新增记录
BrokerageRecordDO record = BrokerageRecordConvert.INSTANCE.convert(user, bizType, bizId, 0, brokeragePrice,
null, title, null, null);
brokerageRecordMapper.insert(record);
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean unfreezeRecord(BrokerageRecordDO record) { public boolean unfreezeRecord(BrokerageRecordDO record) {
// 更新记录状态 // 更新记录状态

View File

@ -70,8 +70,9 @@ public interface BrokerageUserService {
* *
* @param id 用户编号 * @param id 用户编号
* @param price 用户可用佣金 * @param price 用户可用佣金
* @return 更新结果
*/ */
void updateUserPrice(Long id, Integer price); boolean updateUserPrice(Long id, Integer price);
/** /**
* 更新用户冻结佣金 * 更新用户冻结佣金

View File

@ -110,12 +110,13 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
} }
@Override @Override
public void updateUserPrice(Long id, Integer price) { public boolean updateUserPrice(Long id, Integer price) {
if (price > 0) { if (price > 0) {
brokerageUserMapper.updatePriceIncr(id, price); brokerageUserMapper.updatePriceIncr(id, price);
} else if (price < 0) { } else if (price < 0) {
brokerageUserMapper.updatePriceDecr(id, price); return brokerageUserMapper.updatePriceDecr(id, price) > 0;
} }
return true;
} }
@Override @Override

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.service.brokerage;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
@ -13,7 +14,7 @@ import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum
public interface BrokerageWithdrawService { public interface BrokerageWithdrawService {
/** /**
* 审核佣金提现 * 管理员审核佣金提现
* *
* @param id 佣金编号 * @param id 佣金编号
* @param status 审核状态 * @param status 审核状态
@ -37,4 +38,13 @@ public interface BrokerageWithdrawService {
* @return 佣金提现分页 * @return 佣金提现分页
*/ */
PageResult<BrokerageWithdrawDO> getBrokerageWithdrawPage(BrokerageWithdrawPageReqVO pageReqVO); PageResult<BrokerageWithdrawDO> getBrokerageWithdrawPage(BrokerageWithdrawPageReqVO pageReqVO);
/**
* 会员创建佣金提现
*
* @param createReqVO 创建信息
* @param userId 会员用户编号
* @return 佣金提现编号
*/
Long createBrokerageWithdraw(AppBrokerageWithdrawCreateReqVO createReqVO, Long userId);
} }

View File

@ -2,26 +2,34 @@ package cn.iocoder.yudao.module.trade.service.brokerage;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.math.Money;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.withdraw.AppBrokerageWithdrawCreateReqVO;
import cn.iocoder.yudao.module.trade.convert.brokerage.BrokerageWithdrawConvert;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.dal.dataobject.config.TradeConfigDO;
import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper; import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
import cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants; import cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum; import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawStatusEnum;
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageWithdrawTypeEnum;
import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Validator;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_NOT_EXISTS; import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING;
/** /**
* 佣金提现 Service 实现类 * 佣金提现 Service 实现类
@ -37,10 +45,15 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
@Resource @Resource
private BrokerageRecordService brokerageRecordService; private BrokerageRecordService brokerageRecordService;
@Resource
private TradeConfigService tradeConfigService;
@Resource @Resource
private NotifyMessageSendApi notifyMessageSendApi; private NotifyMessageSendApi notifyMessageSendApi;
@Resource
private Validator validator;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason) { public void auditBrokerageWithdraw(Integer id, BrokerageWithdrawStatusEnum status, String auditReason) {
@ -61,19 +74,26 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING); throw exception(BROKERAGE_WITHDRAW_STATUS_NOT_AUDITING);
} }
// 3. 驳回时需要退还用户佣金
String templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_APPROVE; String templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_APPROVE;
if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) { if (BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.equals(status)) {
// 3.1 通过时佣金转余额
if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
// todo
}
} else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) {
templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_REJECT; templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_REJECT;
// todo @owen // 3.2 驳回时需要退还用户佣金
// brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW, withdraw.getPrice(), ""); brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());
} else {
throw new IllegalArgumentException("不支持的提现状态");
} }
// 4. 通知用户 // 4. 通知用户
Map<String, Object> templateParams = MapUtil.<String, Object>builder() Map<String, Object> templateParams = MapUtil.<String, Object>builder()
.put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime())) .put("createTime", LocalDateTimeUtil.formatNormal(withdraw.getCreateTime()))
.put("price", String.format("%.2f", withdraw.getPrice() / 100d)) .put("price", new Money(0, withdraw.getPrice()).toString())
.put("reason", withdraw.getAuditReason()) .put("reason", withdraw.getAuditReason())
.build(); .build();
NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO() NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO()
@ -100,4 +120,53 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
return brokerageWithdrawMapper.selectPage(pageReqVO); return brokerageWithdrawMapper.selectPage(pageReqVO);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Long createBrokerageWithdraw(AppBrokerageWithdrawCreateReqVO createReqVO, Long userId) {
// 校验提现金额
TradeConfigDO tradeConfig = validateWithdrawPrice(createReqVO.getPrice());
// 校验提现参数
createReqVO.validate(validator);
// 计算手续费
Integer feePrice = calculateFeePrice(createReqVO.getPrice(), tradeConfig.getBrokerageWithdrawFeePercent());
// 创建佣金提现记录
BrokerageWithdrawDO withdraw = BrokerageWithdrawConvert.INSTANCE.convert(createReqVO, userId, feePrice);
brokerageWithdrawMapper.insert(withdraw);
// 创建用户佣金记录
brokerageRecordService.addBrokerage(userId, BrokerageRecordBizTypeEnum.WITHDRAW, String.valueOf(withdraw.getId()),
-createReqVO.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW.getTitle());
return withdraw.getId();
}
/**
* 计算提现手续费
*
* @param withdrawPrice 提现金额
* @param percent 手续费百分比
* @return 提现手续费
*/
Integer calculateFeePrice(Integer withdrawPrice, Integer percent) {
Integer feePrice = 0;
if (percent != null && percent > 0) {
feePrice = MoneyUtils.calculateRatePrice(withdrawPrice, Double.valueOf(percent));
}
return feePrice;
}
/**
* 校验提现金额要求
*
* @param withdrawPrice 提现金额
* @return 分销配置
*/
TradeConfigDO validateWithdrawPrice(Integer withdrawPrice) {
TradeConfigDO tradeConfig = tradeConfigService.getTradeConfig();
if (tradeConfig.getBrokerageWithdrawMinPrice() != null && withdrawPrice < tradeConfig.getBrokerageWithdrawMinPrice()) {
throw exception(BROKERAGE_WITHDRAW_MIN_PRICE, new Money(0, tradeConfig.getBrokerageWithdrawMinPrice()));
}
return tradeConfig;
}
} }

View File

@ -2,14 +2,18 @@ package cn.iocoder.yudao.module.trade.service.brokerage;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO; import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.withdraw.BrokerageWithdrawPageReqVO;
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO; import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageWithdrawDO;
import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper; import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageWithdrawMapper;
import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Validator;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId; import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
@ -32,6 +36,19 @@ public class BrokerageWithdrawServiceImplTest extends BaseDbUnitTest {
@Resource @Resource
private BrokerageWithdrawMapper brokerageWithdrawMapper; private BrokerageWithdrawMapper brokerageWithdrawMapper;
@MockBean
private BrokerageRecordService brokerageRecordService;
@MockBean
private BrokerageUserService brokerageUserService;
@MockBean
private TradeConfigService tradeConfigService;
@MockBean
private NotifyMessageSendApi notifyMessageSendApi;
@Resource
private Validator validator;
@Test @Test
@Disabled // TODO 请修改 null 为需要的值然后删除 @Disabled 注解 @Disabled // TODO 请修改 null 为需要的值然后删除 @Disabled 注解
public void testGetBrokerageWithdrawPage() { public void testGetBrokerageWithdrawPage() {
@ -84,4 +101,17 @@ public class BrokerageWithdrawServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(dbBrokerageWithdraw, pageResult.getList().get(0)); assertPojoEquals(dbBrokerageWithdraw, pageResult.getList().get(0));
} }
@Test
public void testCalculateFeePrice() {
Integer withdrawPrice = 100;
// 测试手续费比例未设置
Integer percent = null;
assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
// 测试手续费给为0
percent = 0;
assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 0);
// 测试手续费
percent = 1;
assertEquals(brokerageWithdrawService.calculateFeePrice(withdrawPrice, percent), 1);
}
} }

View File

@ -186,6 +186,6 @@ CREATE TABLE IF NOT EXISTS "trade_brokerage_withdraw"
"updater" varchar DEFAULT '', "updater" varchar DEFAULT '',
"update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE, "deleted" bit NOT NULL DEFAULT FALSE,
"tenant_id" bigint not null default '0', "tenant_id" bigint not null default '0',
PRIMARY KEY ("id") PRIMARY KEY ("id")
) COMMENT '佣金提现'; ) COMMENT '佣金提现';