111
This commit is contained in:
1 parent
074307b07b
commit
a099e72517
47 files changed
+1901
-65
No files matched your search
+20
@@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.style;
|
||||
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment;
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment;
|
||||
|
||||
/**
|
||||
* Excel 导出的全局样式配置。
|
||||
*/
|
||||
public final class ExcelExportStyleTokens {
|
||||
|
||||
/** 表头与数据行的统一高度,单位为磅。 */
|
||||
public static final short ROW_HEIGHT = 32;
|
||||
|
||||
public static final HorizontalAlignment HEAD_HORIZONTAL_ALIGNMENT = HorizontalAlignment.CENTER;
|
||||
|
||||
public static final VerticalAlignment VERTICAL_ALIGNMENT = VerticalAlignment.CENTER;
|
||||
|
||||
private ExcelExportStyleTokens() {
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -2,9 +2,14 @@ package cn.iocoder.yudao.framework.excel.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.converters.longconverter.LongStringConverter;
|
||||
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
|
||||
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
|
||||
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
|
||||
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
|
||||
import com.alibaba.excel.write.style.row.SimpleRowHeightStyleStrategy;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
@@ -18,6 +23,19 @@ import java.util.List;
|
||||
*/
|
||||
public class ExcelUtils {
|
||||
|
||||
/** 创建使用全局行高及表头样式的导出构建器,支持响应流和分批文件导出。 */
|
||||
public static ExcelWriterBuilder writeBuilder(Class<?> head) {
|
||||
WriteCellStyle headStyle = new WriteCellStyle();
|
||||
headStyle.setHorizontalAlignment(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
headStyle.setVerticalAlignment(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
WriteCellStyle contentStyle = new WriteCellStyle();
|
||||
contentStyle.setVerticalAlignment(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
return EasyExcel.write().head(head)
|
||||
.registerWriteHandler(new SimpleRowHeightStyleStrategy(
|
||||
ExcelExportStyleTokens.ROW_HEIGHT, ExcelExportStyleTokens.ROW_HEIGHT))
|
||||
.registerWriteHandler(new HorizontalCellStyleStrategy(headStyle, contentStyle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将列表以 Excel 响应给前端
|
||||
*
|
||||
@@ -32,7 +50,7 @@ public class ExcelUtils {
|
||||
public static <T> void write(HttpServletResponse response, String filename, String sheetName,
|
||||
Class<T> head, List<T> data) throws IOException {
|
||||
// 输出 Excel
|
||||
EasyExcel.write(response.getOutputStream(), head)
|
||||
writeBuilder(head).file(response.getOutputStream())
|
||||
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
|
||||
.registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package cn.iocoder.yudao.framework.excel.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
|
||||
import com.alibaba.excel.annotation.write.style.HeadStyle;
|
||||
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
|
||||
import com.alibaba.excel.enums.poi.VerticalAlignmentEnum;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ExcelUtilsTest {
|
||||
|
||||
@Test
|
||||
void sharedExportUsesGlobalHeightAndCenteredHeadersAndPreservesContentFormatting() throws Exception {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
ExcelUtils.write(response, "会员.xlsx", "会员", ExportRow.class,
|
||||
Collections.singletonList(new ExportRow(9007199254740993L, "测试会员")));
|
||||
|
||||
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertHeaderStyle(sheet);
|
||||
Row row = sheet.getRow(2);
|
||||
assertThat(row.getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
assertThat(row.getCell(0).getStringCellValue()).isEqualTo("9007199254740993");
|
||||
assertThat(row.getCell(1).getStringCellValue()).isEqualTo("测试会员");
|
||||
assertThat(row.getCell(1).getCellStyle().getAlignment()).isEqualTo(HorizontalAlignment.RIGHT);
|
||||
assertThat(row.getCell(1).getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
assertThat(workbook.getFontAt(sheet.getRow(0).getCell(0).getCellStyle().getFontIndex()).getBold()).isTrue();
|
||||
}
|
||||
assertThat(response.getHeader("Content-Disposition")).startsWith("attachment;filename=");
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyExportsAlsoUseGlobalHeightAndCenteredMultilevelHeaders() throws Exception {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
ExcelUtils.write(response, "会员.xlsx", "会员", ExportRow.class, Collections.emptyList());
|
||||
|
||||
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertThat(sheet.getLastRowNum()).isEqualTo(1);
|
||||
assertHeaderStyle(sheet);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertHeaderStyle(Sheet sheet) {
|
||||
for (int rowIndex = 0; rowIndex < 2; rowIndex++) {
|
||||
Row header = sheet.getRow(rowIndex);
|
||||
assertThat(header.getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
for (Cell cell : header) {
|
||||
assertThat(cell.getCellStyle().getAlignment()).isEqualTo(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
assertThat(cell.getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@HeadRowHeight(12)
|
||||
@ContentRowHeight(13)
|
||||
@HeadStyle(horizontalAlignment = HorizontalAlignmentEnum.LEFT, verticalAlignment = VerticalAlignmentEnum.TOP)
|
||||
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.RIGHT, verticalAlignment = VerticalAlignmentEnum.TOP)
|
||||
public static class ExportRow {
|
||||
|
||||
@ExcelProperty({"会员信息", "用户编号"})
|
||||
private Long id;
|
||||
|
||||
@ExcelProperty({"会员信息", "昵称"})
|
||||
private String nickname;
|
||||
}
|
||||
}
|
||||
+15
@@ -1,17 +1,32 @@
|
||||
package cn.iocoder.yudao.module.trade.api.brokerage;
|
||||
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageWechatTransferDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分销记录 API 接口
|
||||
*/
|
||||
public interface TradeBrokerageRecordApi {
|
||||
|
||||
/** 批量查询会员自身收益,包含零收益会员,金额单位为分。 */
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> getIncomeSummaryMap(Collection<Long> userIds);
|
||||
|
||||
/** 按来源会员查询其为指定收款会员产生的收益,金额单位为分。 */
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> getContributionSummaryMap(Long recipientUserId,
|
||||
Collection<Long> sourceUserIds);
|
||||
|
||||
/** 按业务类型和业务编号批量查询每单实际收益人及各状态佣金。 */
|
||||
Map<String, BrokerageOrderSummaryRespDTO> getOrderSummaryMap(BrokerageRecordBizTypeEnum bizType,
|
||||
Collection<String> bizIds);
|
||||
|
||||
/**
|
||||
* 添加朋友圈分销【多级分佣】
|
||||
* 首次分销才有用
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package cn.iocoder.yudao.module.trade.api.brokerage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分销收益汇总,金额单位为分。
|
||||
*/
|
||||
@Data
|
||||
public class BrokerageIncomeSummaryRespDTO {
|
||||
|
||||
/** 已结算的累计收益,提现不扣减。 */
|
||||
private Long settledPrice = 0L;
|
||||
|
||||
/** 待结算收益。 */
|
||||
private Long pendingPrice = 0L;
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package cn.iocoder.yudao.module.trade.api.brokerage.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 单笔业务的分销佣金,按实际收益人和分销层级汇总,金额单位为分。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BrokerageOrderSummaryRespDTO extends BrokerageIncomeSummaryRespDTO {
|
||||
|
||||
private Long cancelledPrice = 0L;
|
||||
|
||||
private List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class Recipient extends BrokerageIncomeSummaryRespDTO {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private Integer sourceUserLevel;
|
||||
|
||||
private Long cancelledPrice = 0L;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.module.trade.enums.brokerage;
|
||||
|
||||
/**
|
||||
* 分销收益查询共用的条件。状态、逻辑删除及租户由查询方处理。
|
||||
*/
|
||||
public final class BrokerageIncomeSql {
|
||||
|
||||
public static final String CONDITION = condition("");
|
||||
|
||||
private BrokerageIncomeSql() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param alias 代码中固定的表别名,空字符串表示无别名
|
||||
*/
|
||||
public static String condition(String alias) {
|
||||
String prefix = alias.isEmpty() ? "" : alias + ".";
|
||||
// 历史数据的业务类型存在错位,沿用佣金列表对提现文本的排除规则。
|
||||
return prefix + "biz_type IN (1, 2) AND " + prefix + "price > 0"
|
||||
+ " AND COALESCE(" + prefix + "title, '') NOT LIKE '%提现%'"
|
||||
+ " AND COALESCE(" + prefix + "description, '') NOT LIKE '%提现%'";
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -13,6 +13,8 @@ import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
|
||||
import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
|
||||
import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageWechatTransferDTO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageUserDO;
|
||||
@@ -49,6 +51,23 @@ public class TradeBrokerageRecordApiImpl implements TradeBrokerageRecordApi {
|
||||
@Resource
|
||||
private PayTransferApi payTransferApi;
|
||||
|
||||
@Override
|
||||
public Map<Long, BrokerageIncomeSummaryRespDTO> getIncomeSummaryMap(Collection<Long> userIds) {
|
||||
return brokerageRecordService.getIncomeSummaryMap(userIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, BrokerageIncomeSummaryRespDTO> getContributionSummaryMap(Long recipientUserId,
|
||||
Collection<Long> sourceUserIds) {
|
||||
return brokerageRecordService.getContributionSummaryMap(recipientUserId, sourceUserIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, BrokerageOrderSummaryRespDTO> getOrderSummaryMap(BrokerageRecordBizTypeEnum bizType,
|
||||
Collection<String> bizIds) {
|
||||
return brokerageRecordService.getOrderSummaryMap(bizType, bizIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加朋友圈分销【多级分佣】
|
||||
* 首次分销才有用
|
||||
|
||||
+9
@@ -20,9 +20,18 @@ public class BrokerageRecordPageReqVO extends PageParam {
|
||||
@Schema(description = "用户编号", example = "25973")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "来源会员编号", example = "1125")
|
||||
private Long sourceUserId;
|
||||
|
||||
@Schema(description = "仅查询分销收益记录")
|
||||
private Boolean incomeOnly;
|
||||
|
||||
@Schema(description = "业务类型", example = "1")
|
||||
private Integer bizType;
|
||||
|
||||
@Schema(description = "业务编号,会员佣金对应会员开通记录编号")
|
||||
private String bizId;
|
||||
|
||||
@Schema(description = "状态", example = "1")
|
||||
private Integer status;
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.trade.controller.admin.order.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
|
||||
@@ -14,8 +15,8 @@ import lombok.experimental.Accessors;
|
||||
@Schema(description = "管理后台 - 商家发货模版 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadRowHeight(25)
|
||||
@ContentRowHeight(25)
|
||||
@HeadRowHeight(ExcelExportStyleTokens.ROW_HEIGHT)
|
||||
@ContentRowHeight(ExcelExportStyleTokens.ROW_HEIGHT)
|
||||
@Accessors(chain = false)
|
||||
public class TradeMchDeliveryImportVo {
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.trade.controller.admin.order.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
@@ -16,8 +17,8 @@ import java.time.LocalDateTime;
|
||||
@Schema(description = "管理后台 - 订单导出 Response VO")
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@HeadRowHeight(25)
|
||||
@ContentRowHeight(25)
|
||||
@HeadRowHeight(ExcelExportStyleTokens.ROW_HEIGHT)
|
||||
@ContentRowHeight(ExcelExportStyleTokens.ROW_HEIGHT)
|
||||
public class TradeOrderListExportVo {
|
||||
|
||||
@ExcelProperty("订单编号")
|
||||
|
||||
+74
-1
@@ -1,14 +1,20 @@
|
||||
package cn.iocoder.yudao.module.trade.dal.mysql.brokerage;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageIncomeSql;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
|
||||
import cn.iocoder.yudao.module.trade.service.brokerage.bo.UserBrokerageSummaryRespBO;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.github.yulichang.toolkit.MPJWrappers;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@@ -17,6 +23,7 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -31,11 +38,42 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> {
|
||||
default PageResult<BrokerageRecordDO> selectPage(BrokerageRecordPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<BrokerageRecordDO>()
|
||||
.eqIfPresent(BrokerageRecordDO::getUserId, reqVO.getUserId())
|
||||
.eqIfPresent(BrokerageRecordDO::getSourceUserId, reqVO.getSourceUserId())
|
||||
.eqIfPresent(BrokerageRecordDO::getBizType, reqVO.getBizType())
|
||||
.eqIfPresent(BrokerageRecordDO::getBizId, reqVO.getBizId())
|
||||
.eqIfPresent(BrokerageRecordDO::getStatus, reqVO.getStatus())
|
||||
.eqIfPresent(BrokerageRecordDO::getSourceUserLevel, reqVO.getSourceUserLevel())
|
||||
.betweenIfPresent(BrokerageRecordDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByDesc(BrokerageRecordDO::getId));
|
||||
.orderByDesc(BrokerageRecordDO::getId)
|
||||
.apply(Boolean.TRUE.equals(reqVO.getIncomeOnly()), BrokerageIncomeSql.CONDITION));
|
||||
}
|
||||
|
||||
/** recipientUserId 为空时按收款人汇总,否则按指定收款人的来源会员汇总。 */
|
||||
default Map<Long, BrokerageIncomeSummaryRespDTO> selectIncomeSummaryMap(Collection<Long> userIds,
|
||||
Long recipientUserId) {
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> result = new LinkedHashMap<>();
|
||||
userIds.forEach(userId -> result.put(userId, new BrokerageIncomeSummaryRespDTO()));
|
||||
if (userIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
String groupColumn = recipientUserId == null ? "user_id" : "source_user_id";
|
||||
QueryWrapper<BrokerageRecordDO> query = new QueryWrapper<BrokerageRecordDO>()
|
||||
.select(groupColumn + " AS summaryUserId",
|
||||
"SUM(CASE WHEN status = 1 THEN price ELSE 0 END) AS settledPrice",
|
||||
"SUM(CASE WHEN status = 0 THEN price ELSE 0 END) AS pendingPrice")
|
||||
.in(groupColumn, userIds)
|
||||
.eq(recipientUserId != null, "user_id", recipientUserId)
|
||||
.in("status", BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus(),
|
||||
BrokerageRecordStatusEnum.SETTLEMENT.getStatus())
|
||||
.apply(BrokerageIncomeSql.CONDITION)
|
||||
.groupBy(groupColumn);
|
||||
for (Map<String, Object> row : selectMaps(query)) {
|
||||
BrokerageIncomeSummaryRespDTO summary = new BrokerageIncomeSummaryRespDTO();
|
||||
summary.setSettledPrice(MapUtil.getLong(row, "settledPrice", 0L));
|
||||
summary.setPendingPrice(MapUtil.getLong(row, "pendingPrice", 0L));
|
||||
result.put(MapUtil.getLong(row, "summaryUserId"), summary);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
default List<BrokerageRecordDO> selectListByStatusAndUnfreezeTimeLt(Integer status, LocalDateTime unfreezeTime) {
|
||||
@@ -44,6 +82,41 @@ public interface BrokerageRecordMapper extends BaseMapperX<BrokerageRecordDO> {
|
||||
.lt(BrokerageRecordDO::getUnfreezeTime, unfreezeTime));
|
||||
}
|
||||
|
||||
default Map<String, BrokerageOrderSummaryRespDTO> selectOrderSummaryMap(Integer bizType,
|
||||
Collection<String> bizIds) {
|
||||
Map<String, BrokerageOrderSummaryRespDTO> result = new LinkedHashMap<>();
|
||||
bizIds.forEach(id -> result.put(id, new BrokerageOrderSummaryRespDTO()));
|
||||
if (bizIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
QueryWrapper<BrokerageRecordDO> query = new QueryWrapper<BrokerageRecordDO>()
|
||||
.select("biz_id AS bizId", "user_id AS userId", "source_user_level AS sourceUserLevel",
|
||||
"SUM(CASE WHEN status = 1 THEN price ELSE 0 END) AS settledPrice",
|
||||
"SUM(CASE WHEN status = 0 THEN price ELSE 0 END) AS pendingPrice",
|
||||
"SUM(CASE WHEN status = 2 THEN price ELSE 0 END) AS cancelledPrice")
|
||||
.eq("biz_type", bizType)
|
||||
.in("biz_id", bizIds)
|
||||
.in("status", BrokerageRecordStatusEnum.WAIT_SETTLEMENT.getStatus(),
|
||||
BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), BrokerageRecordStatusEnum.CANCEL.getStatus())
|
||||
.apply(BrokerageIncomeSql.CONDITION)
|
||||
.groupBy("biz_id", "user_id", "source_user_level")
|
||||
.orderByAsc("biz_id", "source_user_level", "user_id");
|
||||
for (Map<String, Object> row : selectMaps(query)) {
|
||||
BrokerageOrderSummaryRespDTO.Recipient recipient = new BrokerageOrderSummaryRespDTO.Recipient();
|
||||
recipient.setUserId(MapUtil.getLong(row, "userId"));
|
||||
recipient.setSourceUserLevel(MapUtil.getInt(row, "sourceUserLevel"));
|
||||
recipient.setSettledPrice(MapUtil.getLong(row, "settledPrice", 0L));
|
||||
recipient.setPendingPrice(MapUtil.getLong(row, "pendingPrice", 0L));
|
||||
recipient.setCancelledPrice(MapUtil.getLong(row, "cancelledPrice", 0L));
|
||||
BrokerageOrderSummaryRespDTO summary = result.get(MapUtil.getStr(row, "bizId"));
|
||||
summary.setSettledPrice(summary.getSettledPrice() + recipient.getSettledPrice());
|
||||
summary.setPendingPrice(summary.getPendingPrice() + recipient.getPendingPrice());
|
||||
summary.setCancelledPrice(summary.getCancelledPrice() + recipient.getCancelledPrice());
|
||||
summary.getRecipients().add(recipient);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
default int updateByIdAndStatus(Integer id, Integer status, BrokerageRecordDO updateObj) {
|
||||
return update(updateObj, new LambdaQueryWrapper<BrokerageRecordDO>()
|
||||
.eq(BrokerageRecordDO::getId, id)
|
||||
|
||||
+10
@@ -1,6 +1,8 @@
|
||||
package cn.iocoder.yudao.module.trade.service.brokerage;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO;
|
||||
@@ -26,6 +28,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
|
||||
*/
|
||||
public interface BrokerageRecordService {
|
||||
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> getIncomeSummaryMap(Collection<Long> userIds);
|
||||
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> getContributionSummaryMap(Long recipientUserId,
|
||||
Collection<Long> sourceUserIds);
|
||||
|
||||
Map<String, BrokerageOrderSummaryRespDTO> getOrderSummaryMap(BrokerageRecordBizTypeEnum bizType,
|
||||
Collection<String> bizIds);
|
||||
|
||||
/**
|
||||
* 获得佣金记录
|
||||
*
|
||||
|
||||
+30
@@ -12,6 +12,8 @@ import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
|
||||
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
|
||||
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.record.AppBrokerageProductPriceRespVO;
|
||||
import cn.iocoder.yudao.module.trade.controller.app.brokerage.vo.user.AppBrokerageUserRankByPriceRespVO;
|
||||
@@ -64,6 +66,34 @@ public class BrokerageRecordServiceImpl implements BrokerageRecordService {
|
||||
@Resource
|
||||
private ProductSkuApi productSkuApi;
|
||||
|
||||
@Override
|
||||
public Map<Long, BrokerageIncomeSummaryRespDTO> getIncomeSummaryMap(Collection<Long> userIds) {
|
||||
if (CollUtil.isEmpty(userIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return brokerageRecordMapper.selectIncomeSummaryMap(userIds, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, BrokerageIncomeSummaryRespDTO> getContributionSummaryMap(Long recipientUserId,
|
||||
Collection<Long> sourceUserIds) {
|
||||
if (CollUtil.isEmpty(sourceUserIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Objects.requireNonNull(recipientUserId, "贡献收益的收款会员不能为空");
|
||||
return brokerageRecordMapper.selectIncomeSummaryMap(sourceUserIds, recipientUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, BrokerageOrderSummaryRespDTO> getOrderSummaryMap(BrokerageRecordBizTypeEnum bizType,
|
||||
Collection<String> bizIds) {
|
||||
if (CollUtil.isEmpty(bizIds)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Objects.requireNonNull(bizType, "佣金业务类型不能为空");
|
||||
return brokerageRecordMapper.selectOrderSummaryMap(bizType.getType(), bizIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrokerageRecordDO getBrokerageRecord(Long id) {
|
||||
return brokerageRecordMapper.selectById(id);
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
package cn.iocoder.yudao.module.trade.service.brokerage;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.module.product.api.sku.ProductSkuApi;
|
||||
import cn.iocoder.yudao.module.product.api.spu.ProductSpuApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.controller.admin.brokerage.vo.record.BrokerageRecordPageReqVO;
|
||||
import cn.iocoder.yudao.module.trade.dal.dataobject.brokerage.BrokerageRecordDO;
|
||||
import cn.iocoder.yudao.module.trade.dal.mysql.brokerage.BrokerageRecordMapper;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageIncomeSql;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.service.config.TradeConfigService;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.sql.DataSource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
|
||||
|
||||
@Import(BrokerageRecordServiceImpl.class)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:brokerage_income;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value;",
|
||||
"spring.sql.init.schema-locations=classpath:/sql/brokerage_income_schema.sql"
|
||||
})
|
||||
@Sql(statements = "DELETE FROM trade_brokerage_record", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
|
||||
class BrokerageIncomeQueryTest extends BaseDbUnitTest {
|
||||
|
||||
@Resource private BrokerageRecordServiceImpl service;
|
||||
@Resource private BrokerageRecordMapper mapper;
|
||||
@Resource private MybatisPlusInterceptor interceptor;
|
||||
@Resource private DataSource dataSource;
|
||||
@MockBean private TradeConfigService tradeConfigService;
|
||||
@MockBean private BrokerageUserService brokerageUserService;
|
||||
@MockBean private ProductSpuApi productSpuApi;
|
||||
@MockBean private ProductSkuApi productSkuApi;
|
||||
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void prepareTenantQueries() {
|
||||
jdbc = new JdbcTemplate(dataSource);
|
||||
interceptor.setInterceptors(Arrays.asList(
|
||||
new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(new TenantProperties())),
|
||||
new PaginationInnerInterceptor()));
|
||||
TenantContextHolder.setTenantId(1L);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void clearTenant() {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsAndEmptyInputsReturnZeroSummaries() {
|
||||
assertThat(new BrokerageIncomeSummaryRespDTO().getSettledPrice()).isZero();
|
||||
assertThat(new BrokerageIncomeSummaryRespDTO().getPendingPrice()).isZero();
|
||||
assertThat(service.getIncomeSummaryMap(Collections.emptyList())).isEmpty();
|
||||
assertThat(service.getContributionSummaryMap(null, Collections.emptyList())).isEmpty();
|
||||
assertThat(service.getIncomeSummaryMap(Arrays.asList(1L, 2L)))
|
||||
.hasSize(2).allSatisfy((id, summary) -> {
|
||||
assertThat(summary.getSettledPrice()).isZero();
|
||||
assertThat(summary.getPendingPrice()).isZero();
|
||||
});
|
||||
assertThatNullPointerException().isThrownBy(() ->
|
||||
service.getContributionSummaryMap(null, Collections.singletonList(2L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cumulativeIncomeUsesLongAndSeparatesPendingFromCancelled() {
|
||||
add(1, 2, 1, 1, Integer.MAX_VALUE, "订单佣金", "获得推广佣金", false, 1);
|
||||
add(1, 3, 2, 1, Integer.MAX_VALUE, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(1, 2, 2, 0, 500, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(1, 2, 1, 2, 700, "订单佣金", "获得推广佣金", false, 1);
|
||||
// 缺少来源会员的历史收益仍计入收款人的累计收益。
|
||||
add(1, 0, 1, 1, 100, null, null, false, 1);
|
||||
|
||||
BrokerageIncomeSummaryRespDTO summary = service.getIncomeSummaryMap(Collections.singletonList(1L)).get(1L);
|
||||
assertThat(summary.getSettledPrice()).isEqualTo(2L * Integer.MAX_VALUE + 100);
|
||||
assertThat(summary.getPendingPrice()).isEqualTo(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentAndHistoricalWithdrawalRecordsAreExcluded() {
|
||||
add(1, 2, 1, 1, 100, "订单佣金", "获得推广佣金", false, 1);
|
||||
add(1, 2, 2, 1, 200, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(1, 0, 2, 1, -50, "提现申请", "提现申请扣除佣金", false, 1);
|
||||
add(1, 0, 3, 1, 50, "提现申请驳回", "返还佣金", false, 1);
|
||||
add(1, 0, 4, 1, 50, "提现申请驳回", "返还佣金", false, 1);
|
||||
add(1, 0, 2, 1, 60, "历史提现返还", "返还佣金", false, 1);
|
||||
add(1, 0, 1, 1, 70, "历史记录", "提现申请驳回,返还佣金", false, 1);
|
||||
add(1, 0, 0, 1, 80, "其他业务", "其他业务", false, 1);
|
||||
add(1, 2, 1, 1, 0, "订单佣金", "获得推广佣金", false, 1);
|
||||
|
||||
BrokerageIncomeSummaryRespDTO summary = service.getIncomeSummaryMap(Collections.singletonList(1L)).get(1L);
|
||||
assertThat(summary.getSettledPrice()).isEqualTo(300);
|
||||
BrokerageRecordPageReqVO query = incomeQuery(1L, null);
|
||||
assertThat(mapper.selectPage(query).getTotal()).isEqualTo(2);
|
||||
Long aliasTotal = jdbc.queryForObject("SELECT SUM(r.price) FROM trade_brokerage_record r WHERE "
|
||||
+ BrokerageIncomeSql.condition("r"), Long.class);
|
||||
assertThat(aliasTotal).isEqualTo(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void contributionUsesStoredRecipientAndExactSourceMember() {
|
||||
add(1, 2, 1, 1, 100, "订单佣金", "获得推广佣金", false, 1);
|
||||
add(1, 2, 2, 0, 50, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(1, 3, 2, 1, 200, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(2, 3, 2, 1, 300, "会员佣金", "获得会员佣金", false, 1);
|
||||
add(4, 2, 1, 1, 400, "订单佣金", "获得推广佣金", false, 1);
|
||||
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> summaries = service.getContributionSummaryMap(1L,
|
||||
Arrays.asList(2L, 3L, 9L));
|
||||
assertThat(summaries.get(2L).getSettledPrice()).isEqualTo(100);
|
||||
assertThat(summaries.get(2L).getPendingPrice()).isEqualTo(50);
|
||||
assertThat(summaries.get(3L).getSettledPrice()).isEqualTo(200);
|
||||
assertThat(summaries.get(9L).getSettledPrice()).isZero();
|
||||
assertThat(service.getIncomeSummaryMap(Collections.singletonList(2L)).get(2L).getSettledPrice())
|
||||
.isEqualTo(300);
|
||||
assertThat(mapper.selectPage(incomeQuery(1L, 2L)).getList())
|
||||
.hasSize(2).allSatisfy(record -> {
|
||||
assertThat(record.getUserId()).isEqualTo(1L);
|
||||
assertThat(record.getSourceUserId()).isEqualTo(2L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregateAndDetailQueriesRespectTenantAndLogicalDeletion() {
|
||||
add(1, 2, 1, 1, 100, "订单佣金", "获得推广佣金", false, 1);
|
||||
add(1, 2, 1, 1, 200, "订单佣金", "获得推广佣金", true, 1);
|
||||
add(1, 2, 1, 1, 400, "订单佣金", "获得推广佣金", false, 2);
|
||||
|
||||
assertThat(service.getIncomeSummaryMap(Collections.singletonList(1L)).get(1L).getSettledPrice())
|
||||
.isEqualTo(100);
|
||||
assertThat(service.getContributionSummaryMap(1L, Collections.singletonList(2L)).get(2L).getSettledPrice())
|
||||
.isEqualTo(100);
|
||||
assertThat(mapper.selectPage(incomeQuery(1L, 2L)).getTotal()).isEqualTo(1);
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
assertThat(service.getIncomeSummaryMap(Collections.singletonList(1L)).get(1L).getSettledPrice())
|
||||
.isEqualTo(400);
|
||||
}
|
||||
|
||||
@Test
|
||||
void incomePaginationReturnsRecordsBeyondTwoThousand() {
|
||||
List<Object[]> rows = new ArrayList<>();
|
||||
for (int index = 0; index < 2105; index++) {
|
||||
rows.add(new Object[]{1, 2, 1, 1, 10, "订单佣金", "获得推广佣金", false, 1});
|
||||
}
|
||||
jdbc.batchUpdate(insertSql(), rows);
|
||||
add(1, 2, 3, 1, 100, "提现申请", "提现申请扣除佣金", false, 1);
|
||||
add(1, 3, 1, 1, 100, "其他来源", "获得推广佣金", false, 1);
|
||||
BrokerageRecordPageReqVO query = incomeQuery(1L, 2L);
|
||||
query.setPageSize(100);
|
||||
query.setPageNo(22);
|
||||
|
||||
PageResult<BrokerageRecordDO> page = mapper.selectPage(query);
|
||||
assertThat(page.getTotal()).isEqualTo(2105);
|
||||
assertThat(page.getList()).hasSize(5);
|
||||
assertThat(page.getList()).extracting(BrokerageRecordDO::getId).isSortedAccordingTo(Collections.reverseOrder());
|
||||
query.setPageNo(1);
|
||||
query.setIncomeOnly(null);
|
||||
assertThat(mapper.selectPage(query).getTotal()).isEqualTo(2106);
|
||||
query.setIncomeOnly(false);
|
||||
assertThat(mapper.selectPage(query).getTotal()).isEqualTo(2106);
|
||||
}
|
||||
|
||||
private BrokerageRecordPageReqVO incomeQuery(Long userId, Long sourceUserId) {
|
||||
BrokerageRecordPageReqVO query = new BrokerageRecordPageReqVO();
|
||||
query.setUserId(userId);
|
||||
query.setSourceUserId(sourceUserId);
|
||||
query.setIncomeOnly(true);
|
||||
return query;
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderSummaryAndDetailsUseTheExactBusinessAndActualRecipientsAcrossAllStatuses() {
|
||||
addOrder("100", 2, 10, 1, 1, 200, false, 1, "会员佣金");
|
||||
addOrder("100", 2, 10, 1, 1, 50, false, 1, "会员佣金");
|
||||
addOrder("100", 2, 20, 2, 0, 100, false, 1, "会员佣金");
|
||||
addOrder("100", 2, 30, 1, 2, 300, false, 1, "历史收益人");
|
||||
addOrder("101", 2, 10, 1, 1, 400, false, 1, "其他开通单");
|
||||
addOrder("100", 1, 10, 1, 1, 999, false, 1, "同编号的商城订单");
|
||||
addOrder("100", 2, 10, 1, 1, 500, false, 1, "历史提现返还");
|
||||
addOrder("100", 2, 10, 1, 1, 600, true, 1, "已删除佣金");
|
||||
addOrder("100", 2, 10, 1, 1, 700, false, 2, "其他租户");
|
||||
addOrder("100", 2, 10, 1, 1, 0, false, 1, "零金额");
|
||||
addOrder("100", 2, 10, 1, 1, -100, false, 1, "负金额");
|
||||
|
||||
Map<String, BrokerageOrderSummaryRespDTO> summaries = service.getOrderSummaryMap(
|
||||
BrokerageRecordBizTypeEnum.OPEN_TYPE, Arrays.asList("100", "101", "999"));
|
||||
BrokerageOrderSummaryRespDTO summary = summaries.get("100");
|
||||
assertThat(summary.getSettledPrice()).isEqualTo(250);
|
||||
assertThat(summary.getPendingPrice()).isEqualTo(100);
|
||||
assertThat(summary.getCancelledPrice()).isEqualTo(300);
|
||||
assertThat(summary.getRecipients()).extracting(BrokerageOrderSummaryRespDTO.Recipient::getUserId)
|
||||
.containsExactly(10L, 30L, 20L);
|
||||
assertThat(summary.getRecipients().get(0).getSettledPrice()).isEqualTo(250);
|
||||
assertThat(summary.getRecipients().get(1).getCancelledPrice()).isEqualTo(300);
|
||||
assertThat(summary.getRecipients().get(2).getSourceUserLevel()).isEqualTo(2);
|
||||
assertThat(summaries.get("101").getSettledPrice()).isEqualTo(400);
|
||||
assertThat(summaries.get("999").getRecipients()).isEmpty();
|
||||
assertThat(summaries.get("999").getSettledPrice()).isZero();
|
||||
|
||||
BrokerageRecordPageReqVO query = incomeQuery(null, null);
|
||||
query.setBizType(2);
|
||||
query.setBizId("100");
|
||||
assertThat(mapper.selectPage(query).getTotal()).isEqualTo(4);
|
||||
query.setStatus(1);
|
||||
assertThat(mapper.selectPage(query).getList()).extracting(BrokerageRecordDO::getPrice).containsExactly(50, 200);
|
||||
query.setStatus(0);
|
||||
assertThat(mapper.selectPage(query).getList()).singleElement().satisfies(record -> assertThat(record.getPrice()).isEqualTo(100));
|
||||
query.setStatus(2);
|
||||
assertThat(mapper.selectPage(query).getList()).singleElement().satisfies(record -> assertThat(record.getPrice()).isEqualTo(300));
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
assertThat(service.getOrderSummaryMap(BrokerageRecordBizTypeEnum.OPEN_TYPE, Collections.singletonList("100"))
|
||||
.get("100").getSettledPrice()).isEqualTo(700);
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderSummarySupportsLongAmountsUnknownLevelsAndEmptyInputs() {
|
||||
assertThat(service.getOrderSummaryMap(null, Collections.emptyList())).isEmpty();
|
||||
assertThatNullPointerException().isThrownBy(() -> service.getOrderSummaryMap(null, Collections.singletonList("100")));
|
||||
addOrder("100", 2, 10, 0, 1, Integer.MAX_VALUE, false, 1, "会员佣金");
|
||||
addOrder("100", 2, 10, 0, 1, Integer.MAX_VALUE, false, 1, "会员佣金");
|
||||
BrokerageOrderSummaryRespDTO summary = service.getOrderSummaryMap(BrokerageRecordBizTypeEnum.OPEN_TYPE,
|
||||
Collections.singletonList("100")).get("100");
|
||||
assertThat(summary.getSettledPrice()).isEqualTo(2L * Integer.MAX_VALUE);
|
||||
assertThat(summary.getPendingPrice()).isZero();
|
||||
assertThat(summary.getCancelledPrice()).isZero();
|
||||
assertThat(summary.getRecipients()).singleElement().satisfies(recipient -> {
|
||||
assertThat(recipient.getSourceUserLevel()).isZero();
|
||||
assertThat(recipient.getSettledPrice()).isEqualTo(2L * Integer.MAX_VALUE);
|
||||
});
|
||||
}
|
||||
|
||||
private void addOrder(String bizId, int bizType, long userId, int level, int status, int price,
|
||||
boolean deleted, long tenantId, String title) {
|
||||
jdbc.update("INSERT INTO trade_brokerage_record "
|
||||
+ "(biz_id, biz_type, user_id, source_user_level, status, price, deleted, tenant_id, title) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
bizId, bizType, userId, level, status, price, deleted, tenantId, title);
|
||||
}
|
||||
|
||||
private void add(long userId, long sourceUserId, int bizType, int status, int price,
|
||||
String title, String description, boolean deleted, long tenantId) {
|
||||
jdbc.update(insertSql(), userId, sourceUserId, bizType, status, price, title, description, deleted, tenantId);
|
||||
}
|
||||
|
||||
private String insertSql() {
|
||||
return "INSERT INTO trade_brokerage_record "
|
||||
+ "(user_id, source_user_id, biz_type, status, price, title, description, deleted, tenant_id) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS "trade_brokerage_record" (
|
||||
"id" int NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"user_id" bigint NOT NULL,
|
||||
"biz_id" varchar DEFAULT '',
|
||||
"biz_type" int NOT NULL,
|
||||
"title" varchar DEFAULT '',
|
||||
"description" varchar DEFAULT '',
|
||||
"price" int NOT NULL,
|
||||
"total_price" int DEFAULT 0,
|
||||
"status" int NOT NULL,
|
||||
"frozen_days" int DEFAULT 0,
|
||||
"unfreeze_time" datetime,
|
||||
"source_user_level" int DEFAULT 0,
|
||||
"source_user_id" bigint DEFAULT 0,
|
||||
"creator" varchar DEFAULT '',
|
||||
"create_time" datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
"updater" varchar DEFAULT '',
|
||||
"update_time" datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted" bit DEFAULT FALSE,
|
||||
"tenant_id" bigint DEFAULT 0
|
||||
);
|
||||
+39
@@ -8,16 +8,27 @@ import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempoten
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserAdminOpenReqVo;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserExportRespVO;
|
||||
import cn.iocoder.yudao.module.member.dal.dataobject.opentype.OpenTypeUserDO;
|
||||
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeService;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserExportService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||
@@ -32,6 +43,9 @@ public class OpenTypeUserController {
|
||||
@Resource
|
||||
private OpenTypeService openTypeService;
|
||||
|
||||
@Resource
|
||||
private MemberUserExportService memberUserExportService;
|
||||
|
||||
@GetMapping("/last-by-user-id")
|
||||
@Operation(summary = "获取最后一条会员开通记录")
|
||||
public CommonResult<OpenTypeUserDO> lastByUserId(@RequestParam("userId") Long userId) {
|
||||
@@ -50,6 +64,31 @@ public class OpenTypeUserController {
|
||||
return success(openTypeService.getOpenTypeUserPage(pageReqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/export-async")
|
||||
@Operation(summary = "按会员开通记录搜索条件提交分批导出任务")
|
||||
public CommonResult<MemberUserExportRespVO> exportHistory(@Valid OpenTypeUserReqVO query) {
|
||||
return success(memberUserExportService.createHistory(query));
|
||||
}
|
||||
|
||||
@GetMapping("/export-progress")
|
||||
@Operation(summary = "查询当前登录人的会员开通记录导出进度")
|
||||
public CommonResult<MemberUserExportRespVO> getExportProgress(@RequestParam("taskId") String taskId) {
|
||||
return success(memberUserExportService.getHistoryProgress(taskId));
|
||||
}
|
||||
|
||||
@GetMapping("/export-download")
|
||||
@Operation(summary = "下载当前登录人的会员开通记录导出文件")
|
||||
public ResponseEntity<FileSystemResource> downloadExport(@RequestParam("taskId") String taskId) throws IOException {
|
||||
Path file = memberUserExportService.getHistoryDownloadFile(taskId);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
|
||||
.filename("会员开通记录.xlsx", StandardCharsets.UTF_8).build().toString())
|
||||
.header(HttpHeaders.CACHE_CONTROL, "no-store")
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.contentLength(Files.size(file))
|
||||
.body(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
@GetMapping("/refund")
|
||||
@Operation(summary = "退款")
|
||||
@Idempotent(timeout = 3, timeUnit = TimeUnit.SECONDS, keyResolver = UserIdempotentKeyResolver.class, message = "正在退款中,请稍后重试")
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser;
|
||||
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 会员开通单的佣金及实际收益人,金额单位为分。 */
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class OpenTypeUserBrokerageRespVO extends BrokerageIncomeSummaryRespDTO {
|
||||
|
||||
private Long cancelledPrice = 0L;
|
||||
|
||||
private List<Recipient> recipients = new ArrayList<>();
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class Recipient extends BrokerageOrderSummaryRespDTO.Recipient {
|
||||
|
||||
private String userCode;
|
||||
|
||||
private String nickname;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private boolean memberExists;
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.format.DateTimeFormat;
|
||||
import com.alibaba.excel.annotation.format.NumberFormat;
|
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@ColumnWidth(22)
|
||||
public class OpenTypeUserExportRowVO {
|
||||
|
||||
@ExcelProperty("记录编号")
|
||||
private String id;
|
||||
@ExcelProperty("用户ID")
|
||||
private String memberUserCode;
|
||||
@ExcelProperty("用户名")
|
||||
private String memberNickname;
|
||||
@ExcelProperty("手机号")
|
||||
private String memberMobile;
|
||||
@ExcelProperty("会员标题")
|
||||
private String vipTitle;
|
||||
@ExcelProperty("会员时长(天)")
|
||||
private String vipDuration;
|
||||
@ExcelProperty("金额(元)")
|
||||
private BigDecimal payPrice;
|
||||
@ExcelProperty("支付状态")
|
||||
private String payStatus;
|
||||
@ExcelProperty("充值时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime payTime;
|
||||
@ExcelProperty("支付订单编号")
|
||||
private String payOrderId;
|
||||
@ExcelProperty("支付渠道")
|
||||
private String payChannelCode;
|
||||
@ExcelProperty("退款状态")
|
||||
private String refundStatus;
|
||||
@ExcelProperty("退款金额(元)")
|
||||
private BigDecimal refundPayPrice;
|
||||
@ExcelProperty("退款时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime refundTime;
|
||||
@ExcelProperty("创建时间")
|
||||
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@ExcelProperty("已结算佣金(元)")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal brokerageSettledPrice;
|
||||
@ExcelProperty("待结算佣金(元)")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal brokeragePendingPrice;
|
||||
@ExcelProperty("已取消佣金(元)")
|
||||
@NumberFormat("0.00")
|
||||
private BigDecimal brokerageCancelledPrice;
|
||||
@ExcelProperty("分销明细")
|
||||
@ColumnWidth(200)
|
||||
private String brokerageDetails;
|
||||
}
|
||||
+26
@@ -3,10 +3,14 @@ package cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@Data
|
||||
public class OpenTypeUserReqVO extends PageParam {
|
||||
|
||||
@@ -25,7 +29,29 @@ public class OpenTypeUserReqVO extends PageParam {
|
||||
@Schema(description = "会员名称")
|
||||
private String memberName;
|
||||
|
||||
@Schema(description = "用户 ID,App 我的页面展示的 ID")
|
||||
private String userCode;
|
||||
|
||||
@Schema(description = "手机号,支持模糊查询")
|
||||
private String memberMobile;
|
||||
|
||||
@Schema(description = "会员标题,支持模糊查询")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "充值时间,按支付成功时间筛选")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] payTime;
|
||||
|
||||
@Schema(description = "退款时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] refundTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
|
||||
private List<LocalDate> createTimeList;
|
||||
|
||||
}
|
||||
+8
@@ -53,6 +53,9 @@ public class OpenTypeUserRespVO {
|
||||
@Schema(description = "退款时间")
|
||||
private LocalDateTime refundTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "退款状态")
|
||||
private Integer refundStatus;
|
||||
|
||||
@@ -61,6 +64,8 @@ public class OpenTypeUserRespVO {
|
||||
*/
|
||||
private String memberNickname;
|
||||
|
||||
private String memberUserCode;
|
||||
|
||||
private String memberAvatar;
|
||||
|
||||
private String memberMobile;
|
||||
@@ -71,4 +76,7 @@ public class OpenTypeUserRespVO {
|
||||
private String vipTitle;
|
||||
|
||||
private String vipDuration;
|
||||
|
||||
@Schema(description = "本单实际产生的分销佣金及收益人")
|
||||
private OpenTypeUserBrokerageRespVO brokerageSummary = new OpenTypeUserBrokerageRespVO();
|
||||
}
|
||||
+35
-1
@@ -25,6 +25,8 @@ import cn.iocoder.yudao.module.member.service.user.MemberUserExportService;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageUserApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageUserRespDTO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -48,6 +50,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -86,6 +89,8 @@ public class MemberUserController {
|
||||
private AddressService addressService;
|
||||
@Resource
|
||||
private TradeBrokerageUserApi tradeBrokerageUserApi;
|
||||
@Resource
|
||||
private TradeBrokerageRecordApi tradeBrokerageRecordApi;
|
||||
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "更新会员用户")
|
||||
@@ -138,6 +143,7 @@ public class MemberUserController {
|
||||
// 查询用户tag
|
||||
if (ObjectUtil.isNotEmpty(result)) {
|
||||
fillInviters(Collections.singletonList(result));
|
||||
fillDetailBrokerageSummaries(result);
|
||||
List<MemberRecentTagDO> memberRecentTags = memberRecentTagMapper.getByMemberId(result.getId());
|
||||
if (ObjectUtil.isNotEmpty(memberRecentTags)) {
|
||||
List<MemberUserRespVO.MemberTag> tags = memberRecentTags.stream().map(v -> {
|
||||
@@ -158,7 +164,7 @@ public class MemberUserController {
|
||||
public CommonResult<PageResult<MemberUserRespVO>> getUserPage(@Valid MemberUserPageReqVO pageVO) {
|
||||
PageResult<MemberUserDO> pageResult = memberUserService.getUserPage(pageVO);
|
||||
if (CollUtil.isEmpty(pageResult.getList())) {
|
||||
return success(PageResult.empty());
|
||||
return success(new PageResult<>(Collections.emptyList(), pageResult.getTotal()));
|
||||
}
|
||||
// 处理用户标签返显
|
||||
Set<Long> tagIds = pageResult.getList().stream()
|
||||
@@ -180,6 +186,10 @@ public class MemberUserController {
|
||||
PageResult<MemberUserRespVO> result = MemberUserConvert.INSTANCE.convertPage(
|
||||
pageResult, tags, memberRecentTags, levels, groups, memberCfgList, addresses);
|
||||
fillInviters(result.getList());
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> summaries = tradeBrokerageRecordApi.getIncomeSummaryMap(
|
||||
convertSet(result.getList(), MemberUserRespVO::getId));
|
||||
result.getList().forEach(user -> user.setBrokerageSummary(
|
||||
summaries.getOrDefault(user.getId(), new BrokerageIncomeSummaryRespDTO())));
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@@ -196,7 +206,15 @@ public class MemberUserController {
|
||||
Map<Long, MemberUserDO> users = convertMap(memberUserService.getUserList(
|
||||
convertSet(page.getList(), BrokerageUserRespDTO::getId)), MemberUserDO::getId);
|
||||
PageResult<MemberUserBrokerageChildRespVO> result = BeanUtils.toBean(page, MemberUserBrokerageChildRespVO.class);
|
||||
Set<Long> childIds = convertSet(page.getList(), BrokerageUserRespDTO::getId);
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> summaries = tradeBrokerageRecordApi.getIncomeSummaryMap(childIds);
|
||||
Long contributionUserId = reqVO.getContributionUserId() == null
|
||||
? reqVO.getBindUserId() : reqVO.getContributionUserId();
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> contributions = tradeBrokerageRecordApi.getContributionSummaryMap(
|
||||
contributionUserId, childIds);
|
||||
result.getList().forEach(child -> {
|
||||
child.setBrokerageSummary(summaries.getOrDefault(child.getId(), new BrokerageIncomeSummaryRespDTO()));
|
||||
child.setContributionSummary(contributions.getOrDefault(child.getId(), new BrokerageIncomeSummaryRespDTO()));
|
||||
MemberUserDO user = users.get(child.getId());
|
||||
child.setMemberExists(user != null);
|
||||
if (user != null) {
|
||||
@@ -209,6 +227,22 @@ public class MemberUserController {
|
||||
return success(result);
|
||||
}
|
||||
|
||||
private void fillDetailBrokerageSummaries(MemberUserRespVO user) {
|
||||
Set<Long> userIds = new HashSet<>(Collections.singleton(user.getId()));
|
||||
MemberUserInviterRespVO inviter = user.getInviter();
|
||||
if (inviter != null) {
|
||||
userIds.add(inviter.getId());
|
||||
}
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> summaries = tradeBrokerageRecordApi.getIncomeSummaryMap(userIds);
|
||||
user.setBrokerageSummary(summaries.getOrDefault(user.getId(), new BrokerageIncomeSummaryRespDTO()));
|
||||
if (inviter != null) {
|
||||
inviter.setBrokerageSummary(summaries.getOrDefault(inviter.getId(), new BrokerageIncomeSummaryRespDTO()));
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> contributions = tradeBrokerageRecordApi.getContributionSummaryMap(
|
||||
user.getId(), Collections.singleton(inviter.getId()));
|
||||
inviter.setContributionSummary(contributions.getOrDefault(inviter.getId(), new BrokerageIncomeSummaryRespDTO()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillInviters(List<MemberUserRespVO> users) {
|
||||
List<BrokerageUserRespDTO> relations = tradeBrokerageUserApi.getBrokerageUserList(
|
||||
convertSet(users, MemberUserRespVO::getId));
|
||||
|
||||
+7
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.user.vo;
|
||||
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -29,4 +30,10 @@ public class MemberUserBrokerageChildRespVO {
|
||||
@Schema(description = "会员资料是否存在")
|
||||
private Boolean memberExists;
|
||||
|
||||
@Schema(description = "该会员的分销收益汇总,金额单位为分")
|
||||
private BrokerageIncomeSummaryRespDTO brokerageSummary = new BrokerageIncomeSummaryRespDTO();
|
||||
|
||||
@Schema(description = "该会员贡献给指定会员的收益汇总,金额单位为分")
|
||||
private BrokerageIncomeSummaryRespDTO contributionSummary = new BrokerageIncomeSummaryRespDTO();
|
||||
|
||||
}
|
||||
+4
@@ -17,6 +17,10 @@ public class MemberUserBrokerageChildrenPageReqVO extends PageParam {
|
||||
@Positive(message = "邀请人编号必须为正数")
|
||||
private Long bindUserId;
|
||||
|
||||
@Schema(description = "收益贡献的接收会员编号,未传时使用邀请人编号")
|
||||
@Positive(message = "收益接收会员编号必须为正数")
|
||||
private Long contributionUserId;
|
||||
|
||||
public MemberUserBrokerageChildrenPageReqVO() {
|
||||
setPageSize(20);
|
||||
}
|
||||
|
||||
-7
@@ -4,10 +4,6 @@ import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.format.DateTimeFormat;
|
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
|
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
|
||||
import com.alibaba.excel.annotation.write.style.ContentStyle;
|
||||
import com.alibaba.excel.enums.poi.VerticalAlignmentEnum;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -15,9 +11,6 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@ColumnWidth(22)
|
||||
@HeadRowHeight(25)
|
||||
@ContentRowHeight(24)
|
||||
@ContentStyle(verticalAlignment = VerticalAlignmentEnum.CENTER)
|
||||
public class MemberUserExportRowVO {
|
||||
|
||||
@ExcelProperty("用户ID")
|
||||
|
||||
+7
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.user.vo;
|
||||
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -25,4 +26,10 @@ public class MemberUserInviterRespVO {
|
||||
@Schema(description = "邀请人的会员资料是否存在")
|
||||
private Boolean memberExists;
|
||||
|
||||
@Schema(description = "邀请人的分销收益汇总,金额单位为分")
|
||||
private BrokerageIncomeSummaryRespDTO brokerageSummary = new BrokerageIncomeSummaryRespDTO();
|
||||
|
||||
@Schema(description = "邀请人贡献给当前会员的收益汇总,金额单位为分")
|
||||
private BrokerageIncomeSummaryRespDTO contributionSummary = new BrokerageIncomeSummaryRespDTO();
|
||||
|
||||
}
|
||||
+18
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.user.vo;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -9,6 +10,8 @@ import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
@@ -59,6 +62,21 @@ public class MemberUserPageReqVO extends PageParam {
|
||||
@Schema(description = "是否禁用:0 未禁用,1 已禁用", example = "1")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "累计已结算分销收益下限,单位为分,包含边界", example = "100")
|
||||
@Min(value = 0, message = "分销收益下限不能小于零")
|
||||
private Long brokeragePriceMin;
|
||||
|
||||
@Schema(description = "累计已结算分销收益上限,单位为分,包含边界", example = "10000")
|
||||
@Min(value = 0, message = "分销收益上限不能小于零")
|
||||
private Long brokeragePriceMax;
|
||||
|
||||
@JsonIgnore
|
||||
@AssertTrue(message = "分销收益下限不能大于上限")
|
||||
public boolean isBrokeragePriceRangeValid() {
|
||||
return brokeragePriceMin == null || brokeragePriceMax == null
|
||||
|| brokeragePriceMin <= brokeragePriceMax;
|
||||
}
|
||||
|
||||
// TODO 芋艿:注册用户类型;
|
||||
|
||||
// TODO 芋艿:登录用户类型;
|
||||
|
||||
+4
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.member.controller.admin.user.vo;
|
||||
|
||||
import cn.iocoder.yudao.module.member.controller.admin.recent.vo.RecentPageResVo;
|
||||
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberRecentCfgDO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -25,6 +26,9 @@ public class MemberUserRespVO extends MemberUserBaseVO {
|
||||
@Schema(description = "直接邀请人,未绑定时为空")
|
||||
private MemberUserInviterRespVO inviter;
|
||||
|
||||
@Schema(description = "分销收益汇总,金额单位为分")
|
||||
private BrokerageIncomeSummaryRespDTO brokerageSummary = new BrokerageIncomeSummaryRespDTO();
|
||||
|
||||
@Schema(description = "微信号", example = "wx_user")
|
||||
private String wechatNum;
|
||||
|
||||
|
||||
+11
-1
@@ -46,4 +46,14 @@ public interface OpenTypeUserMapper extends BaseMapperX<OpenTypeUserDO> {
|
||||
* 获得会员开通记录分页
|
||||
*/
|
||||
IPage<OpenTypeUserRespVO> page(IPage<Page> page, @Param("dto") OpenTypeUserReqVO dto);
|
||||
}
|
||||
|
||||
Long selectExportUpperBound(@Param("dto") OpenTypeUserReqVO dto);
|
||||
|
||||
long selectExportCount(@Param("dto") OpenTypeUserReqVO dto, @Param("upperId") Long upperId);
|
||||
|
||||
List<OpenTypeUserRespVO> selectExportBatch(@Param("dto") OpenTypeUserReqVO dto,
|
||||
@Param("upperId") Long upperId,
|
||||
@Param("lastCreateTime") LocalDateTime lastCreateTime,
|
||||
@Param("lastId") Long lastId,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
+12
@@ -10,6 +10,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageIncomeSql;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordStatusEnum;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@@ -129,6 +131,16 @@ public interface MemberUserMapper extends BaseMapperX<MemberUserDO> {
|
||||
.eqIfPresent(MemberUserDO::getSex, reqVO.getSex())
|
||||
.eqIfPresent(MemberUserDO::getStatus, reqVO.getStatus())
|
||||
.apply(StrUtil.isNotEmpty(tagIdSql), tagIdSql);
|
||||
if (reqVO.getBrokeragePriceMin() != null || reqVO.getBrokeragePriceMax() != null) {
|
||||
// COALESCE 放在子查询内,保证租户拦截器能够解析并隔离收益记录。
|
||||
String incomeSql = "(SELECT COALESCE(SUM(income.price), 0) FROM trade_brokerage_record income"
|
||||
+ " WHERE income.user_id = member_user.id AND income.deleted = 0 AND "
|
||||
+ BrokerageIncomeSql.condition("income") + " AND income.status = {0})";
|
||||
query.apply(reqVO.getBrokeragePriceMin() != null, incomeSql + " >= {1}",
|
||||
BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), reqVO.getBrokeragePriceMin());
|
||||
query.apply(reqVO.getBrokeragePriceMax() != null, incomeSql + " <= {1}",
|
||||
BrokerageRecordStatusEnum.SETTLEMENT.getStatus(), reqVO.getBrokeragePriceMax());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(reqVO.getMemberRecentTagList())) {
|
||||
String placeholders = IntStream.range(0, reqVO.getMemberRecentTagList().size())
|
||||
.mapToObj(index -> "{" + index + "}")
|
||||
|
||||
+9
-8
@@ -27,7 +27,7 @@ public class MemberUserExportRedisDAO {
|
||||
stringRedisTemplate.opsForValue().set(taskKey(task.getTenantId(), task.getUserId(), task.getTaskId()),
|
||||
JsonUtils.toJsonString(task), Duration.ofHours(properties.getRetentionHours()));
|
||||
// 每批续期,长时间导出不会使防重复提交锁提前失效。
|
||||
stringRedisTemplate.expire(activeKey(task.getTenantId(), task.getUserId()), Duration.ofHours(properties.getRetentionHours()));
|
||||
stringRedisTemplate.expire(activeKey(task.getTenantId(), task.getUserId(), task.getExportType()), Duration.ofHours(properties.getRetentionHours()));
|
||||
}
|
||||
|
||||
public MemberUserExportTask get(Long tenantId, Long userId, String taskId) {
|
||||
@@ -35,17 +35,17 @@ public class MemberUserExportRedisDAO {
|
||||
MemberUserExportTask.class);
|
||||
}
|
||||
|
||||
public boolean acquire(Long tenantId, Long userId, String taskId) {
|
||||
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(activeKey(tenantId, userId), taskId,
|
||||
public boolean acquire(Long tenantId, Long userId, String taskId, String exportType) {
|
||||
return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(activeKey(tenantId, userId, exportType), taskId,
|
||||
Duration.ofHours(properties.getRetentionHours())));
|
||||
}
|
||||
|
||||
public String getActiveTaskId(Long tenantId, Long userId) {
|
||||
return stringRedisTemplate.opsForValue().get(activeKey(tenantId, userId));
|
||||
public String getActiveTaskId(Long tenantId, Long userId, String exportType) {
|
||||
return stringRedisTemplate.opsForValue().get(activeKey(tenantId, userId, exportType));
|
||||
}
|
||||
|
||||
public void release(MemberUserExportTask task) {
|
||||
stringRedisTemplate.execute(RELEASE_SCRIPT, Collections.singletonList(activeKey(task.getTenantId(), task.getUserId())),
|
||||
stringRedisTemplate.execute(RELEASE_SCRIPT, Collections.singletonList(activeKey(task.getTenantId(), task.getUserId(), task.getExportType())),
|
||||
task.getTaskId());
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ public class MemberUserExportRedisDAO {
|
||||
return "member:user:export:task:" + tenantId + ":" + userId + ":" + taskId;
|
||||
}
|
||||
|
||||
private String activeKey(Long tenantId, Long userId) {
|
||||
return "member:user:export:active:" + tenantId + ":" + userId;
|
||||
private String activeKey(Long tenantId, Long userId, String exportType) {
|
||||
String key = "member:user:export:active:" + tenantId + ":" + userId;
|
||||
return exportType == null ? key : key + ":" + exportType;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -60,6 +60,9 @@ public interface OpenTypeService {
|
||||
*/
|
||||
PageResult<OpenTypeUserRespVO> getOpenTypeUserPage(OpenTypeUserReqVO pageReqVO);
|
||||
|
||||
/** 批量填充开通单的实际分销收益,供列表和分批导出共用。 */
|
||||
void fillBrokerageSummary(List<OpenTypeUserRespVO> records);
|
||||
|
||||
/**
|
||||
* 获得会员类列表
|
||||
*/
|
||||
@@ -111,4 +114,4 @@ public interface OpenTypeService {
|
||||
* 退款的回调
|
||||
*/
|
||||
void updateWalletRechargeRefunded(Long merchantOrderId, Long payRefundId);
|
||||
}
|
||||
}
|
||||
+43
-4
@@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.member.client.vo.chat.privatemsg.ChatPrivateTotal
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openType.OpenTypePageReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openType.OpenTypeSaveReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserAdminOpenReqVo;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserBrokerageRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.app.opentype.vo.AppPayOpenReqType;
|
||||
@@ -35,9 +36,7 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageAddDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
@@ -60,10 +59,16 @@ import cn.iocoder.yudao.module.member.dal.mysql.opentype.OpenTypeMapper;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.hutool.core.util.ObjectUtil.notEqual;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.fenToYuanStr;
|
||||
@@ -197,6 +202,7 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
public PageResult<OpenTypeUserRespVO> getOpenTypeUserPage(OpenTypeUserReqVO pageReqVO) {
|
||||
IPage<OpenTypeUserRespVO> pageList = openTypeUserMapper.page(new Page(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO);
|
||||
List<OpenTypeUserRespVO> list = pageList.getRecords();
|
||||
fillBrokerageSummary(list);
|
||||
|
||||
PageResult<OpenTypeUserRespVO> result = new PageResult<OpenTypeUserRespVO>();
|
||||
result.setList(list);
|
||||
@@ -204,6 +210,39 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillBrokerageSummary(List<OpenTypeUserRespVO> records) {
|
||||
if (records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> bizIds = records.stream().map(record -> record.getId().toString()).collect(Collectors.toList());
|
||||
Map<String, BrokerageOrderSummaryRespDTO> summaries = tradeBrokerageRecordApi.getOrderSummaryMap(
|
||||
BrokerageRecordBizTypeEnum.OPEN_TYPE, bizIds);
|
||||
Set<Long> recipientIds = summaries.values().stream().flatMap(summary -> summary.getRecipients().stream())
|
||||
.map(BrokerageOrderSummaryRespDTO.Recipient::getUserId).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
Map<Long, MemberUserDO> members = recipientIds.isEmpty() ? Collections.emptyMap()
|
||||
: convertMap(memberUserService.getUserList(recipientIds), MemberUserDO::getId);
|
||||
for (OpenTypeUserRespVO record : records) {
|
||||
BrokerageOrderSummaryRespDTO source = summaries.getOrDefault(record.getId().toString(), new BrokerageOrderSummaryRespDTO());
|
||||
OpenTypeUserBrokerageRespVO summary = new OpenTypeUserBrokerageRespVO();
|
||||
summary.setSettledPrice(source.getSettledPrice());
|
||||
summary.setPendingPrice(source.getPendingPrice());
|
||||
summary.setCancelledPrice(source.getCancelledPrice());
|
||||
summary.setRecipients(source.getRecipients().stream().map(recipient -> {
|
||||
OpenTypeUserBrokerageRespVO.Recipient result = BeanUtils.toBean(recipient, OpenTypeUserBrokerageRespVO.Recipient.class);
|
||||
MemberUserDO member = members.get(recipient.getUserId());
|
||||
result.setMemberExists(member != null);
|
||||
if (member != null) {
|
||||
result.setUserCode(member.getUserCode());
|
||||
result.setNickname(member.getNickname());
|
||||
result.setAvatar(member.getAvatar());
|
||||
}
|
||||
return result;
|
||||
}).collect(Collectors.toList()));
|
||||
record.setBrokerageSummary(summary);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得会员类列表
|
||||
*/
|
||||
@@ -569,4 +608,4 @@ public class OpenTypeServiceImpl implements OpenTypeService {
|
||||
throw exception(MemberErrorCode.OPEN_TYPE_NOT_EXISTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package cn.iocoder.yudao.module.member.service.opentype;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserBrokerageRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserExportRowVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.dal.mysql.opentype.OpenTypeUserMapper;
|
||||
import cn.iocoder.yudao.module.member.framework.export.config.MemberUserExportProperties;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.ExcelWriter;
|
||||
import com.alibaba.excel.write.metadata.WriteSheet;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class OpenTypeUserExportWriter {
|
||||
|
||||
@Resource
|
||||
private OpenTypeUserMapper openTypeUserMapper;
|
||||
@Resource
|
||||
private MemberUserExportProperties properties;
|
||||
@Resource
|
||||
private OpenTypeService openTypeService;
|
||||
|
||||
public long write(Path file, OpenTypeUserReqVO query, BiConsumer<Long, Long> progress) {
|
||||
Long upperId = openTypeUserMapper.selectExportUpperBound(query);
|
||||
long total = upperId == null ? 0 : openTypeUserMapper.selectExportCount(query, upperId);
|
||||
progress.accept(total, 0L);
|
||||
long processed = 0;
|
||||
Long lastId = null;
|
||||
LocalDateTime lastCreateTime = null;
|
||||
int sheetIndex = 0;
|
||||
int sheetRows = 0;
|
||||
try (ExcelWriter writer = ExcelUtils.writeBuilder(OpenTypeUserExportRowVO.class)
|
||||
.file(file.toFile()).inMemory(false).build()) {
|
||||
WriteSheet sheet = EasyExcel.writerSheet(sheetIndex, "会员开通记录1").build();
|
||||
writer.write(Collections.emptyList(), sheet);
|
||||
while (upperId != null) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
throw new IllegalStateException("导出线程已中断");
|
||||
}
|
||||
List<OpenTypeUserRespVO> records = openTypeUserMapper.selectExportBatch(query, upperId,
|
||||
lastCreateTime, lastId, properties.getBatchSize());
|
||||
if (records.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
openTypeService.fillBrokerageSummary(records);
|
||||
List<OpenTypeUserExportRowVO> rows = records.stream().map(this::buildRow).collect(Collectors.toList());
|
||||
int offset = 0;
|
||||
while (offset < rows.size()) {
|
||||
if (sheetRows == properties.getRowsPerSheet()) {
|
||||
sheetIndex++;
|
||||
sheet = EasyExcel.writerSheet(sheetIndex, "会员开通记录" + (sheetIndex + 1)).build();
|
||||
sheetRows = 0;
|
||||
}
|
||||
int end = Math.min(rows.size(), offset + properties.getRowsPerSheet() - sheetRows);
|
||||
writer.write(rows.subList(offset, end), sheet);
|
||||
sheetRows += end - offset;
|
||||
offset = end;
|
||||
}
|
||||
OpenTypeUserRespVO last = records.get(records.size() - 1);
|
||||
lastId = last.getId();
|
||||
lastCreateTime = last.getCreateTime();
|
||||
processed += records.size();
|
||||
progress.accept(Math.max(total, processed), processed);
|
||||
}
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
private OpenTypeUserExportRowVO buildRow(OpenTypeUserRespVO record) {
|
||||
OpenTypeUserExportRowVO row = new OpenTypeUserExportRowVO();
|
||||
row.setId(record.getId().toString());
|
||||
row.setMemberUserCode(record.getMemberUserCode());
|
||||
row.setMemberNickname(record.getMemberNickname());
|
||||
row.setMemberMobile(record.getMemberMobile());
|
||||
row.setVipTitle(record.getVipTitle());
|
||||
row.setVipDuration(record.getVipDuration());
|
||||
row.setPayPrice(record.getPayPrice() == null ? null : BigDecimal.valueOf(record.getPayPrice(), 2));
|
||||
row.setPayStatus(record.getPayTime() != null || Boolean.TRUE.equals(record.getPayStatus()) ? "支付成功" : "未支付");
|
||||
row.setPayTime(record.getPayTime());
|
||||
row.setPayOrderId(record.getPayOrderId() == null ? null : record.getPayOrderId().toString());
|
||||
row.setPayChannelCode(record.getPayChannelCode());
|
||||
row.setRefundStatus(Arrays.stream(PayRefundStatusEnum.values())
|
||||
.filter(status -> Objects.equals(status.getStatus(), record.getRefundStatus()))
|
||||
.map(PayRefundStatusEnum::getName)
|
||||
.findFirst().orElse(record.getRefundStatus() == null ? "未退款" : record.getRefundStatus().toString()));
|
||||
row.setRefundPayPrice(record.getRefundPayPrice() == null ? null : BigDecimal.valueOf(record.getRefundPayPrice(), 2));
|
||||
row.setRefundTime(record.getRefundTime());
|
||||
row.setCreateTime(record.getCreateTime());
|
||||
OpenTypeUserBrokerageRespVO summary = record.getBrokerageSummary();
|
||||
row.setBrokerageSettledPrice(BigDecimal.valueOf(summary.getSettledPrice(), 2));
|
||||
row.setBrokeragePendingPrice(BigDecimal.valueOf(summary.getPendingPrice(), 2));
|
||||
row.setBrokerageCancelledPrice(BigDecimal.valueOf(summary.getCancelledPrice(), 2));
|
||||
row.setBrokerageDetails(summary.getRecipients().stream().map(this::formatRecipient).collect(Collectors.joining(";")));
|
||||
return row;
|
||||
}
|
||||
|
||||
private String formatRecipient(OpenTypeUserBrokerageRespVO.Recipient recipient) {
|
||||
String level = recipient.getSourceUserLevel() == null || recipient.getSourceUserLevel() <= 0
|
||||
? "层级未知" : recipient.getSourceUserLevel() + "级分销";
|
||||
String nickname = StrUtil.blankToDefault(recipient.getNickname(), recipient.isMemberExists() ? "未设置昵称" : "会员资料缺失");
|
||||
String userCode = StrUtil.blankToDefault(recipient.getUserCode(), Objects.toString(recipient.getUserId(), "未知"));
|
||||
return String.format("%s %s(ID %s) 已结算 %s元,待结算 %s元,已取消 %s元", level, nickname, userCode,
|
||||
BigDecimal.valueOf(recipient.getSettledPrice(), 2).toPlainString(),
|
||||
BigDecimal.valueOf(recipient.getPendingPrice(), 2).toPlainString(),
|
||||
BigDecimal.valueOf(recipient.getCancelledPrice(), 2).toPlainString());
|
||||
}
|
||||
}
|
||||
+2
@@ -11,5 +11,7 @@ public class MemberUserExportTask extends MemberUserExportRespVO {
|
||||
private Long userId;
|
||||
private Long tenantId;
|
||||
private String workerId;
|
||||
/** null 兼容已有会员用户导出任务。 */
|
||||
private String exportType;
|
||||
private long updatedAt;
|
||||
}
|
||||
+40
-11
@@ -4,10 +4,12 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserExportRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
import cn.iocoder.yudao.module.member.dal.redis.user.MemberUserExportRedisDAO;
|
||||
import cn.iocoder.yudao.module.member.framework.export.config.MemberUserExportProperties;
|
||||
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeUserExportWriter;
|
||||
import cn.iocoder.yudao.module.member.service.user.BO.MemberUserExportTask;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -29,6 +31,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
@@ -37,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
|
||||
@Slf4j
|
||||
public class MemberUserExportService {
|
||||
|
||||
private static final String HISTORY_EXPORT_TYPE = "open-type-history";
|
||||
private final String workerId = UUID.randomUUID().toString();
|
||||
private final Set<String> liveWorkers = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@@ -45,11 +50,22 @@ public class MemberUserExportService {
|
||||
@Resource
|
||||
private MemberUserExportWriter exportWriter;
|
||||
@Resource
|
||||
private OpenTypeUserExportWriter historyExportWriter;
|
||||
@Resource
|
||||
private MemberUserExportProperties properties;
|
||||
@Resource(name = "memberUserExportExecutor")
|
||||
private Executor executor;
|
||||
|
||||
public MemberUserExportRespVO create(MemberUserPageReqVO query) {
|
||||
return create(null, "会员用户", (file, progress) -> exportWriter.write(file, query, progress));
|
||||
}
|
||||
|
||||
public MemberUserExportRespVO createHistory(OpenTypeUserReqVO query) {
|
||||
return create(HISTORY_EXPORT_TYPE, "会员开通记录", (file, progress) -> historyExportWriter.write(file, query, progress));
|
||||
}
|
||||
|
||||
private MemberUserExportRespVO create(String exportType, String fileNamePrefix,
|
||||
BiFunction<Path, BiConsumer<Long, Long>, Long> writer) {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (userId == null) {
|
||||
@@ -59,16 +75,17 @@ public class MemberUserExportService {
|
||||
task.setTaskId(UUID.randomUUID().toString());
|
||||
task.setUserId(userId);
|
||||
task.setTenantId(tenantId);
|
||||
task.setExportType(exportType);
|
||||
task.setWorkerId(workerId + ":" + task.getTaskId());
|
||||
task.setStatus("WAITING");
|
||||
task.setMessage("导出任务已提交,等待处理");
|
||||
task.setFileName("会员用户_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + ".xlsx");
|
||||
if (!exportRedisDAO.acquire(tenantId, userId, task.getTaskId())) {
|
||||
String activeId = exportRedisDAO.getActiveTaskId(tenantId, userId);
|
||||
task.setFileName(fileNamePrefix + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + ".xlsx");
|
||||
if (!exportRedisDAO.acquire(tenantId, userId, task.getTaskId(), exportType)) {
|
||||
String activeId = exportRedisDAO.getActiveTaskId(tenantId, userId, exportType);
|
||||
if (activeId == null) {
|
||||
throw invalidParamException("导出任务状态正在更新,请重试");
|
||||
}
|
||||
MemberUserExportTask active = getOwnedTask(activeId);
|
||||
MemberUserExportTask active = getOwnedTask(activeId, exportType);
|
||||
if (isActive(active)) {
|
||||
MemberUserExportRespVO response = toResponse(active);
|
||||
response.setExistingTask(true);
|
||||
@@ -85,7 +102,7 @@ public class MemberUserExportService {
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(SecurityFrameworkUtils.getAuthentication());
|
||||
executor.execute(new DelegatingSecurityContextRunnable(
|
||||
() -> TenantUtils.execute(tenantId, () -> runExport(task, query)), context));
|
||||
() -> TenantUtils.execute(tenantId, () -> runExport(task, writer)), context));
|
||||
} catch (RejectedExecutionException exception) {
|
||||
liveWorkers.remove(task.getWorkerId());
|
||||
fail(task, "当前导出任务较多,请稍后重试");
|
||||
@@ -100,11 +117,23 @@ public class MemberUserExportService {
|
||||
}
|
||||
|
||||
public MemberUserExportRespVO getProgress(String taskId) {
|
||||
return toResponse(getOwnedTask(taskId));
|
||||
return toResponse(getOwnedTask(taskId, null));
|
||||
}
|
||||
|
||||
public MemberUserExportRespVO getHistoryProgress(String taskId) {
|
||||
return toResponse(getOwnedTask(taskId, HISTORY_EXPORT_TYPE));
|
||||
}
|
||||
|
||||
public Path getDownloadFile(String taskId) {
|
||||
MemberUserExportTask task = getOwnedTask(taskId);
|
||||
return getDownloadFile(taskId, null);
|
||||
}
|
||||
|
||||
public Path getHistoryDownloadFile(String taskId) {
|
||||
return getDownloadFile(taskId, HISTORY_EXPORT_TYPE);
|
||||
}
|
||||
|
||||
private Path getDownloadFile(String taskId, String exportType) {
|
||||
MemberUserExportTask task = getOwnedTask(taskId, exportType);
|
||||
if (!"SUCCESS".equals(task.getStatus())) {
|
||||
throw invalidParamException("导出文件尚未生成,请等待任务完成");
|
||||
}
|
||||
@@ -115,14 +144,14 @@ public class MemberUserExportService {
|
||||
return file;
|
||||
}
|
||||
|
||||
private void runExport(MemberUserExportTask task, MemberUserPageReqVO query) {
|
||||
private void runExport(MemberUserExportTask task, BiFunction<Path, BiConsumer<Long, Long>, Long> writer) {
|
||||
Path partial = filePath(task.getTaskId(), ".part");
|
||||
try {
|
||||
Files.createDirectories(partial.getParent());
|
||||
task.setStatus("RUNNING");
|
||||
task.setMessage("正在统计并分批导出");
|
||||
exportRedisDAO.save(task);
|
||||
long processed = exportWriter.write(partial, query, (total, count) -> {
|
||||
long processed = writer.apply(partial, (total, count) -> {
|
||||
task.setTotal(total);
|
||||
task.setProcessed(count);
|
||||
task.setProgress(total == 0 ? 0 : (int) Math.min(99, count * 100 / total));
|
||||
@@ -147,7 +176,7 @@ public class MemberUserExportService {
|
||||
}
|
||||
}
|
||||
|
||||
private MemberUserExportTask getOwnedTask(String taskId) {
|
||||
private MemberUserExportTask getOwnedTask(String taskId, String exportType) {
|
||||
if (taskId == null || !taskId.matches("[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}")) {
|
||||
throw invalidParamException("导出任务不存在或已过期,请重新导出");
|
||||
}
|
||||
@@ -155,7 +184,7 @@ public class MemberUserExportService {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
MemberUserExportTask task = exportRedisDAO.get(tenantId, userId, taskId);
|
||||
if (task == null || userId == null || !Objects.equals(userId, task.getUserId())
|
||||
|| !Objects.equals(tenantId, task.getTenantId())) {
|
||||
|| !Objects.equals(tenantId, task.getTenantId()) || !Objects.equals(exportType, task.getExportType())) {
|
||||
throw invalidParamException("导出任务不存在或已过期,请重新导出");
|
||||
}
|
||||
if (isActive(task) && System.currentTimeMillis() - task.getUpdatedAt() > TimeUnit.MINUTES.toMillis(2)
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.member.service.user;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
|
||||
import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserExportRowVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
@@ -51,7 +52,8 @@ public class MemberUserExportWriter {
|
||||
Long lastId = null;
|
||||
int sheetIndex = 0;
|
||||
int sheetRows = 0;
|
||||
try (ExcelWriter writer = EasyExcel.write(file.toFile(), MemberUserExportRowVO.class).inMemory(false).build()) {
|
||||
try (ExcelWriter writer = ExcelUtils.writeBuilder(MemberUserExportRowVO.class)
|
||||
.file(file.toFile()).inMemory(false).build()) {
|
||||
WriteSheet sheet = newSheet(sheetIndex);
|
||||
writer.write(Collections.emptyList(), sheet); // 零结果也生成包含列头的合法文件。
|
||||
while (upperBound != null) {
|
||||
|
||||
+91
-18
@@ -31,15 +31,67 @@
|
||||
<if test="dto.memberId != null">
|
||||
and a.member_id = #{dto.memberId}
|
||||
</if>
|
||||
<if test="dto.payStatus != null and dto.payStatus != '' ">
|
||||
and a.pay_status = #{dto.payStatus}
|
||||
<if test="dto.payStatus != null">
|
||||
<choose>
|
||||
<when test="dto.payStatus">
|
||||
and (a.pay_status = 1 or a.pay_time is not null)
|
||||
</when>
|
||||
<otherwise>
|
||||
and (a.pay_status = 0 or a.pay_status is null) and a.pay_time is null
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="dto.refundStatus != null and dto.refundStatus != '' ">
|
||||
and a.refund_status = #{dto.refundStatus}
|
||||
<if test="dto.refundStatus != null and dto.refundStatus != ''">
|
||||
<choose>
|
||||
<when test='dto.refundStatus == "0"'>
|
||||
and (a.refund_status is null or a.refund_status = 0)
|
||||
</when>
|
||||
<otherwise>
|
||||
and a.refund_status = #{dto.refundStatus}
|
||||
</otherwise>
|
||||
</choose>
|
||||
</if>
|
||||
<if test="dto.createTimeList != null and dto.createTimeList.size() != 0">
|
||||
and a.createTime between #{dto.createTimeList[0]} and #{dto.createTimeList[1]}
|
||||
<if test="dto.memberName != null and dto.memberName != ''">
|
||||
and u.nickname like concat('%', #{dto.memberName}, '%')
|
||||
</if>
|
||||
<if test="dto.userCode != null and dto.userCode != ''">
|
||||
and u.user_code = #{dto.userCode}
|
||||
</if>
|
||||
<if test="dto.memberMobile != null and dto.memberMobile != ''">
|
||||
and u.mobile like concat('%', #{dto.memberMobile}, '%')
|
||||
</if>
|
||||
<if test="dto.title != null and dto.title != ''">
|
||||
and o.title like concat('%', #{dto.title}, '%')
|
||||
</if>
|
||||
<if test="dto.payTime != null and dto.payTime.length == 2">
|
||||
and a.pay_time between #{dto.payTime[0]} and #{dto.payTime[1]}
|
||||
</if>
|
||||
<if test="dto.refundTime != null and dto.refundTime.length == 2">
|
||||
and a.refund_time between #{dto.refundTime[0]} and #{dto.refundTime[1]}
|
||||
</if>
|
||||
<if test="dto.createTime != null and dto.createTime.length == 2">
|
||||
and a.create_time between #{dto.createTime[0]} and #{dto.createTime[1]}
|
||||
</if>
|
||||
<if test="dto.createTimeList != null and dto.createTimeList.size() == 2">
|
||||
and a.create_time >= #{dto.createTimeList[0]}
|
||||
and a.create_time < date_add(#{dto.createTimeList[1]}, interval 1 day)
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<sql id="historyJoins">
|
||||
from member_open_type_user a
|
||||
left join member_user u on a.member_id = u.id
|
||||
left join member_open_type o on a.open_type_id = o.id
|
||||
</sql>
|
||||
|
||||
<sql id="historyColumns">
|
||||
<include refid="selectA"/>,
|
||||
u.nickname as memberNickname,
|
||||
u.user_code as memberUserCode,
|
||||
u.mobile as memberMobile,
|
||||
u.avatar as memberAvatar,
|
||||
o.title as vipTitle,
|
||||
o.duration as vipDuration
|
||||
</sql>
|
||||
|
||||
<select id="getExtByMemeberIdList" resultType="cn.iocoder.yudao.module.member.service.opentype.BO.OpenTypeExtBO">
|
||||
@@ -64,21 +116,42 @@
|
||||
|
||||
<select id="page"
|
||||
resultType="cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO">
|
||||
<include refid="selectA"/>,
|
||||
u.nickname as memberNickname,
|
||||
u.mobile as memberMobile,
|
||||
u.avatar as memberAvatar,
|
||||
o.title as vipTitle,
|
||||
o.duration as vipDuration
|
||||
from member_open_type_user a
|
||||
left join member_user u on a.member_id = u.id
|
||||
left join member_open_type o on a.open_type_id = o.id
|
||||
<include refid="historyColumns"/>
|
||||
<include refid="historyJoins"/>
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
<if test="dto.memberName != null">
|
||||
and u.nickname like concat('%', #{dto.memberName}, '%')
|
||||
</if>
|
||||
</where>
|
||||
order by a.create_time desc , a.id desc
|
||||
</select>
|
||||
|
||||
<select id="selectExportUpperBound" resultType="java.lang.Long">
|
||||
select max(a.id)
|
||||
<include refid="historyJoins"/>
|
||||
<where><include refid="whereA"/></where>
|
||||
</select>
|
||||
|
||||
<select id="selectExportCount" resultType="java.lang.Long">
|
||||
select count(*)
|
||||
<include refid="historyJoins"/>
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
and a.id <= #{upperId}
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<select id="selectExportBatch"
|
||||
resultType="cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO">
|
||||
<include refid="historyColumns"/>
|
||||
<include refid="historyJoins"/>
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
and a.id <= #{upperId}
|
||||
<if test="lastId != null">
|
||||
and (a.create_time < #{lastCreateTime}
|
||||
or (a.create_time = #{lastCreateTime} and a.id < #{lastId}))
|
||||
</if>
|
||||
</where>
|
||||
order by a.create_time desc, a.id desc
|
||||
limit #{limit}
|
||||
</select>
|
||||
</mapper>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package cn.iocoder.yudao.module.member.controller.admin.opentype;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserExportService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
class OpenTypeUserExportControllerTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private OpenTypeUserController controller;
|
||||
@Mock
|
||||
private MemberUserExportService service;
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void bindsAllFiltersAndFalsePaymentStatusWithIndexedDateRanges() throws Exception {
|
||||
MockMvcBuilders.standaloneSetup(controller).build()
|
||||
.perform(post("/member/open-type/history/export-async")
|
||||
.param("memberName", "测试会员").param("memberMobile", "139")
|
||||
.param("userCode", "000001").param("title", "月卡").param("payStatus", "false")
|
||||
.param("refundStatus", "0")
|
||||
.param("payTime[0]", "2026-11-01 00:00:00").param("payTime[1]", "2026-11-11 23:59:59")
|
||||
.param("refundTime[0]", "2026-11-05 00:00:00").param("refundTime[1]", "2026-11-11 23:59:59")
|
||||
.param("createTime[0]", "2026-10-01 00:00:00").param("createTime[1]", "2026-11-11 23:59:59"))
|
||||
.andExpect(status().isOk());
|
||||
ArgumentCaptor<OpenTypeUserReqVO> captor = ArgumentCaptor.forClass(OpenTypeUserReqVO.class);
|
||||
verify(service).createHistory(captor.capture());
|
||||
OpenTypeUserReqVO query = captor.getValue();
|
||||
assertThat(query.getMemberName()).isEqualTo("测试会员");
|
||||
assertThat(query.getMemberMobile()).isEqualTo("139");
|
||||
assertThat(query.getUserCode()).isEqualTo("000001");
|
||||
assertThat(query.getTitle()).isEqualTo("月卡");
|
||||
assertThat(query.getPayStatus()).isFalse();
|
||||
assertThat(query.getRefundStatus()).isEqualTo("0");
|
||||
assertThat(query.getPayTime()).containsExactly(LocalDateTime.of(2026, 11, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 11, 11, 23, 59, 59));
|
||||
assertThat(query.getRefundTime()).containsExactly(LocalDateTime.of(2026, 11, 5, 0, 0),
|
||||
LocalDateTime.of(2026, 11, 11, 23, 59, 59));
|
||||
assertThat(query.getCreateTime()).containsExactly(LocalDateTime.of(2026, 10, 1, 0, 0),
|
||||
LocalDateTime.of(2026, 11, 11, 23, 59, 59));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadStreamsTheHistoryFile() throws Exception {
|
||||
Path file = Files.write(directory.resolve("history.xlsx"), new byte[]{1, 2, 3});
|
||||
when(service.getHistoryDownloadFile("history-task")).thenReturn(file);
|
||||
MockMvcBuilders.standaloneSetup(controller).build()
|
||||
.perform(get("/member/open-type/history/export-download").param("taskId", "history-task"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.andExpect(header().string("Cache-Control", "no-store"))
|
||||
.andExpect(content().bytes(new byte[]{1, 2, 3}));
|
||||
}
|
||||
}
|
||||
+85
-3
@@ -14,6 +14,8 @@ import cn.iocoder.yudao.module.member.service.user.MemberRecentCfgService;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserExportService;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageUserApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageIncomeSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageUserRespDTO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -34,6 +36,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
@@ -54,6 +58,7 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
@Mock private MemberRecentCfgService memberRecentCfgService;
|
||||
@Mock private AddressService addressService;
|
||||
@Mock private TradeBrokerageUserApi tradeBrokerageUserApi;
|
||||
@Mock private TradeBrokerageRecordApi tradeBrokerageRecordApi;
|
||||
|
||||
@Test
|
||||
void pageLoadsRelationshipsAndInvitersInBatchesAndPreservesMissingMembers() {
|
||||
@@ -65,6 +70,8 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
when(tradeBrokerageUserApi.getBrokerageUserList(anyCollection())).thenReturn(Arrays.asList(
|
||||
relation(1L, 10L), relation(2L, 10L), relation(3L, 99L)));
|
||||
when(memberUserService.getUserList(anyCollection())).thenReturn(Collections.singletonList(user(10L, "邀请人")));
|
||||
when(tradeBrokerageRecordApi.getIncomeSummaryMap(anyCollection())).thenReturn(
|
||||
Collections.singletonMap(1L, summary(12345L, 678L)));
|
||||
|
||||
List<MemberUserRespVO> result = controller.getUserPage(new MemberUserPageReqVO()).getData().getList();
|
||||
|
||||
@@ -77,6 +84,11 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
assertThat(result.get(2).getInviter().getId()).isEqualTo(99L);
|
||||
assertThat(result.get(2).getInviter().getMemberExists()).isFalse();
|
||||
assertThat(result.get(3).getInviter()).isNull();
|
||||
assertThat(result.get(0).getBrokerageSummary().getSettledPrice()).isEqualTo(12345L);
|
||||
assertThat(result.get(0).getBrokerageSummary().getPendingPrice()).isEqualTo(678L);
|
||||
assertThat(result.get(3).getBrokerageSummary().getSettledPrice()).isZero();
|
||||
verify(tradeBrokerageRecordApi).getIncomeSummaryMap(new HashSet<>(Arrays.asList(1L, 2L, 3L, 4L)));
|
||||
verify(tradeBrokerageRecordApi, never()).getContributionSummaryMap(anyLong(), anyCollection());
|
||||
verify(tradeBrokerageUserApi).getBrokerageUserList(new HashSet<>(Arrays.asList(1L, 2L, 3L, 4L)));
|
||||
verify(memberUserService).getUserList(new HashSet<>(Arrays.asList(10L, 99L)));
|
||||
}
|
||||
@@ -96,9 +108,32 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
|
||||
@Test
|
||||
void emptyMemberPageDoesNotQueryRelationships() {
|
||||
when(memberUserService.getUserPage(any())).thenReturn(PageResult.empty());
|
||||
assertThat(controller.getUserPage(new MemberUserPageReqVO()).getData().getList()).isEmpty();
|
||||
verifyNoInteractions(tradeBrokerageUserApi);
|
||||
when(memberUserService.getUserPage(any())).thenReturn(new PageResult<>(Collections.emptyList(), 25L));
|
||||
PageResult<MemberUserRespVO> result = controller.getUserPage(new MemberUserPageReqVO()).getData();
|
||||
assertThat(result.getList()).isEmpty();
|
||||
assertThat(result.getTotal()).isEqualTo(25L);
|
||||
verifyNoInteractions(tradeBrokerageUserApi, tradeBrokerageRecordApi);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailLoadsRootAndInviterIncomeTogetherAndContributionsToRoot() {
|
||||
when(memberUserService.getUser(1L)).thenReturn(user(1L, "会员"));
|
||||
when(tradeBrokerageUserApi.getBrokerageUserList(anyCollection())).thenReturn(Collections.singletonList(relation(1L, 10L)));
|
||||
when(memberUserService.getUserList(anyCollection())).thenReturn(Collections.singletonList(user(10L, "邀请人")));
|
||||
Map<Long, BrokerageIncomeSummaryRespDTO> income = new HashMap<>();
|
||||
income.put(1L, summary(300L, 20L));
|
||||
income.put(10L, summary(400L, 30L));
|
||||
when(tradeBrokerageRecordApi.getIncomeSummaryMap(anyCollection())).thenReturn(income);
|
||||
when(tradeBrokerageRecordApi.getContributionSummaryMap(eq(1L), anyCollection())).thenReturn(
|
||||
Collections.singletonMap(10L, summary(100L, 10L)));
|
||||
|
||||
MemberUserRespVO result = controller.getUser(1L).getData();
|
||||
|
||||
assertThat(result.getBrokerageSummary().getSettledPrice()).isEqualTo(300L);
|
||||
assertThat(result.getInviter().getBrokerageSummary().getSettledPrice()).isEqualTo(400L);
|
||||
assertThat(result.getInviter().getContributionSummary().getSettledPrice()).isEqualTo(100L);
|
||||
verify(tradeBrokerageRecordApi).getIncomeSummaryMap(new HashSet<>(Arrays.asList(1L, 10L)));
|
||||
verify(tradeBrokerageRecordApi).getContributionSummaryMap(1L, Collections.singleton(10L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -114,6 +149,11 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
when(memberUserService.getUserList(anyCollection())).thenReturn(Collections.singletonList(child));
|
||||
MemberUserBrokerageChildrenPageReqVO query = new MemberUserBrokerageChildrenPageReqVO();
|
||||
query.setBindUserId(1L);
|
||||
query.setContributionUserId(100L);
|
||||
when(tradeBrokerageRecordApi.getIncomeSummaryMap(anyCollection())).thenReturn(
|
||||
Collections.singletonMap(20L, summary(300L, 20L)));
|
||||
when(tradeBrokerageRecordApi.getContributionSummaryMap(eq(100L), anyCollection())).thenReturn(
|
||||
Collections.singletonMap(20L, summary(100L, 10L)));
|
||||
|
||||
PageResult<MemberUserBrokerageChildRespVO> result = controller.getBrokerageChildrenPage(query).getData();
|
||||
assertThat(result.getTotal()).isEqualTo(25L);
|
||||
@@ -122,7 +162,22 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
assertThat(result.getList().get(0).getMemberExists()).isTrue();
|
||||
assertThat(result.getList().get(1).getId()).isEqualTo(19L);
|
||||
assertThat(result.getList().get(1).getMemberExists()).isFalse();
|
||||
assertThat(result.getList().get(0).getBrokerageSummary().getSettledPrice()).isEqualTo(300L);
|
||||
assertThat(result.getList().get(0).getContributionSummary().getPendingPrice()).isEqualTo(10L);
|
||||
assertThat(result.getList().get(1).getBrokerageSummary().getSettledPrice()).isZero();
|
||||
assertThat(result.getList().get(1).getContributionSummary().getPendingPrice()).isZero();
|
||||
verify(memberUserService).getUserList(new HashSet<>(Arrays.asList(20L, 19L)));
|
||||
verify(tradeBrokerageRecordApi).getContributionSummaryMap(100L, new HashSet<>(Arrays.asList(20L, 19L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void childContributionDefaultsToImmediateParentForExistingCallers() {
|
||||
when(tradeBrokerageUserApi.getBrokerageUserChildrenPage(eq(1L), any())).thenReturn(
|
||||
new PageResult<>(Collections.singletonList(relation(20L, 1L)), 1L));
|
||||
MemberUserBrokerageChildrenPageReqVO query = new MemberUserBrokerageChildrenPageReqVO();
|
||||
query.setBindUserId(1L);
|
||||
controller.getBrokerageChildrenPage(query);
|
||||
verify(tradeBrokerageRecordApi).getContributionSummaryMap(1L, Collections.singleton(20L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -141,6 +196,26 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
mvc.perform(get("/member/user/brokerage-children-page").param("bindUserId", "0")).andExpect(status().isBadRequest());
|
||||
mvc.perform(get("/member/user/brokerage-children-page").param("bindUserId", "1").param("pageNo", "0"))
|
||||
.andExpect(status().isBadRequest());
|
||||
mvc.perform(get("/member/user/brokerage-children-page").param("bindUserId", "1").param("contributionUserId", "0"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void httpValidatesIncomeRangeAndAllowsZeroAndSingleBound() throws Exception {
|
||||
when(memberUserService.getUserPage(any())).thenReturn(PageResult.empty());
|
||||
MockMvc mvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
mvc.perform(get("/member/user/page").param("brokeragePriceMin", "-1"))
|
||||
.andExpect(status().isBadRequest());
|
||||
mvc.perform(get("/member/user/page").param("brokeragePriceMax", "-1"))
|
||||
.andExpect(status().isBadRequest());
|
||||
mvc.perform(get("/member/user/page").param("brokeragePriceMin", "200").param("brokeragePriceMax", "100"))
|
||||
.andExpect(status().isBadRequest());
|
||||
mvc.perform(get("/member/user/page").param("brokeragePriceMin", "0").param("brokeragePriceMax", "0"))
|
||||
.andExpect(status().isOk());
|
||||
mvc.perform(get("/member/user/page").param("brokeragePriceMin", "3000000000"))
|
||||
.andExpect(status().isOk());
|
||||
verify(memberUserService).getUserPage(argThat(query -> Long.valueOf(3000000000L).equals(query.getBrokeragePriceMin())
|
||||
&& query.getBrokeragePriceMax() == null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -191,6 +266,13 @@ class MemberUserBrokerageControllerTest extends BaseMockitoUnitTest {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BrokerageIncomeSummaryRespDTO summary(Long settled, Long pending) {
|
||||
BrokerageIncomeSummaryRespDTO result = new BrokerageIncomeSummaryRespDTO();
|
||||
result.setSettledPrice(settled);
|
||||
result.setPendingPrice(pending);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static BrokerageUserRespDTO relation(Long id, Long parent) {
|
||||
BrokerageUserRespDTO result = new BrokerageUserRespDTO();
|
||||
result.setId(id);
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package cn.iocoder.yudao.module.member.dal.mysql.opentype;
|
||||
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.mapping.BoundSql;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OpenTypeUserMapperTest {
|
||||
|
||||
private MybatisConfiguration configuration;
|
||||
|
||||
@BeforeEach
|
||||
void loadMapper() throws Exception {
|
||||
configuration = new MybatisConfiguration();
|
||||
try (InputStream source = getClass().getResourceAsStream("/mapper/OpenTypeUserMapper.xml")) {
|
||||
new XMLMapperBuilder(source, configuration, "mapper/OpenTypeUserMapper.xml", configuration.getSqlFragments()).parse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAndEveryExportQueryApplyTheSameFiltersIncludingFalsePaymentStatus() throws Exception {
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
query.setMemberName("会员");
|
||||
query.setMemberMobile("139");
|
||||
query.setUserCode("000001");
|
||||
query.setTitle("月卡");
|
||||
query.setPayStatus(false);
|
||||
query.setPayTime(new LocalDateTime[]{LocalDateTime.of(2026, 11, 1, 0, 0), LocalDateTime.of(2026, 11, 11, 23, 59, 59)});
|
||||
query.setCreateTime(query.getPayTime());
|
||||
query.setRefundTime(query.getPayTime());
|
||||
for (String statement : Arrays.asList("page", "selectExportUpperBound", "selectExportCount", "selectExportBatch")) {
|
||||
BoundSql bound = sql(statement, query, null);
|
||||
String sql = bound.getSql().replaceAll("\\s+", " ");
|
||||
assertThat(sql).contains("u.nickname like", "u.user_code =", "u.mobile like", "o.title like",
|
||||
"a.pay_time between", "a.refund_time between", "a.create_time between", "a.pay_status = 0", "a.pay_time is null");
|
||||
assertThat(bound.getParameterMappings()).extracting(mapping -> mapping.getProperty())
|
||||
.contains("dto.memberName", "dto.memberMobile", "dto.userCode", "dto.title",
|
||||
"dto.payTime[0]", "dto.payTime[1]", "dto.refundTime[0]", "dto.refundTime[1]",
|
||||
"dto.createTime[0]", "dto.createTime[1]");
|
||||
CCJSqlParserUtil.parse(sql);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void paidFilterMatchesTheLegacyDisplayRuleAndEmptyFiltersAreOmitted() {
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
query.setPayStatus(true);
|
||||
assertThat(sql("page", query, null).getSql()).contains("a.pay_status = 1 or a.pay_time is not null");
|
||||
query.setPayStatus(null);
|
||||
query.setMemberName("");
|
||||
assertThat(sql("page", query, null).getSql()).doesNotContain("u.nickname like", "a.pay_status = 1", "a.pay_status = 0",
|
||||
"a.refund_time between");
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsLegacyDateRangeAndUsesCreationTimeAndIdCursorForExportOrder() {
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
query.setCreateTimeList(Arrays.asList(LocalDate.of(2026, 11, 1), LocalDate.of(2026, 11, 11)));
|
||||
String sql = sql("selectExportBatch", query, 50L).getSql().replaceAll("\\s+", " ");
|
||||
assertThat(sql).contains("a.create_time >=", "date_add", "a.create_time <", "a.create_time =", "a.id <",
|
||||
"order by a.create_time desc, a.id desc", "limit ?");
|
||||
assertThat(sql).doesNotContain("a.createTime");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refundFiltersAreSharedByListAndExportAndUnrefundedIncludesLegacyNullStatus() throws Exception {
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
for (String statement : Arrays.asList("page", "selectExportUpperBound", "selectExportCount", "selectExportBatch")) {
|
||||
query.setRefundStatus("0");
|
||||
String sql = sql(statement, query, null).getSql().replaceAll("\\s+", " ");
|
||||
assertThat(sql).contains("(a.refund_status is null or a.refund_status = 0)");
|
||||
CCJSqlParserUtil.parse(sql);
|
||||
for (String refundStatus : Arrays.asList("10", "20")) {
|
||||
query.setRefundStatus(refundStatus);
|
||||
BoundSql bound = sql(statement, query, null);
|
||||
assertThat(bound.getSql()).contains("a.refund_status = ?").doesNotContain("a.refund_status is null");
|
||||
assertThat(bound.getParameterMappings()).extracting(mapping -> mapping.getProperty()).contains("dto.refundStatus");
|
||||
}
|
||||
query.setRefundStatus(null);
|
||||
assertThat(sql(statement, query, null).getSql()).doesNotContain("a.refund_status =");
|
||||
query.setRefundStatus("");
|
||||
assertThat(sql(statement, query, null).getSql()).doesNotContain("a.refund_status =");
|
||||
}
|
||||
}
|
||||
|
||||
private BoundSql sql(String statement, OpenTypeUserReqVO query, Long lastId) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("dto", query);
|
||||
params.put("upperId", 100L);
|
||||
params.put("lastCreateTime", LocalDateTime.of(2026, 11, 10, 12, 0));
|
||||
params.put("lastId", lastId);
|
||||
params.put("limit", 500);
|
||||
return configuration.getMappedStatement(OpenTypeUserMapper.class.getName() + "." + statement).getBoundSql(params);
|
||||
}
|
||||
}
|
||||
+95
@@ -2,6 +2,13 @@ package cn.iocoder.yudao.module.member.dal.mysql.user;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert;
|
||||
@@ -41,6 +48,8 @@ class MemberUserMapperTest extends BaseDbUnitTest {
|
||||
private AddressService addressService;
|
||||
@Resource
|
||||
private DataSource dataSource;
|
||||
@Resource
|
||||
private MybatisPlusInterceptor mybatisPlusInterceptor;
|
||||
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@@ -82,11 +91,27 @@ class MemberUserMapperTest extends BaseDbUnitTest {
|
||||
insertAddress(7L, 999999L, false, false); // 无法识别的历史地区不能显示错误名称。
|
||||
insertAddress(7L, 4295297402L, true, false); // Long 截断后恰好为 330106,不能误显示西湖区。
|
||||
insertAddress(8L, 330106L, true, false);
|
||||
insertIncome(1L, 1, 100, 1, "推广佣金", "收入", false);
|
||||
insertIncome(1L, 2, 200, 1, "会员佣金", "收入", false);
|
||||
insertIncome(1L, 1, 999, 0, "推广佣金", "待结算", false);
|
||||
insertIncome(1L, 1, 999, 1, "推广佣金", "已删除", true);
|
||||
insertIncome(2L, 2, 1000, 1, "会员佣金", "收入", false);
|
||||
insertIncome(2L, 2, 2000, 1, "提现申请", "历史正数提现", false);
|
||||
insertIncome(2L, 1, 999, 2, "推广佣金", "已取消", false);
|
||||
insertIncome(3L, 1, 100, 1, null, null, false);
|
||||
insertIncome(3L, 2, 200, 1, "会员佣金", "收入", false);
|
||||
insertIncome(5L, 4, 999, 1, "提现驳回", "退款", false);
|
||||
insertIncome(5L, 2, 999, 1, "历史流水", "提现申请", false);
|
||||
insertIncome(6L, 1, 999, 0, "推广佣金", "待结算", false);
|
||||
insertIncome(7L, 1, 1500000000, 1, "推广佣金", "收入", false);
|
||||
insertIncome(7L, 2, 1500000000, 1, "会员佣金", "收入", false);
|
||||
insertIncome(8L, 1, 300, 1, "推广佣金", "已删除会员", false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanRecentTags() {
|
||||
jdbcTemplate.update("DELETE FROM member_recent_tag");
|
||||
jdbcTemplate.update("DELETE FROM trade_brokerage_record");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,6 +120,61 @@ class MemberUserMapperTest extends BaseDbUnitTest {
|
||||
7L, 6L, 5L, 4L, 3L, 2L, 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void incomeRangeUsesSettledIncomeAndSupportsInclusiveSingleBounds() {
|
||||
MemberUserPageReqVO request = new MemberUserPageReqVO();
|
||||
request.setBrokeragePriceMin(300L);
|
||||
assertPage(memberUserMapper.selectPage(request), 4L, 7L, 3L, 2L, 1L);
|
||||
request.setBrokeragePriceMax(1000L);
|
||||
assertPage(memberUserMapper.selectPage(request), 3L, 3L, 2L, 1L);
|
||||
request.setBrokeragePriceMin(null);
|
||||
request.setBrokeragePriceMax(0L);
|
||||
assertPage(memberUserMapper.selectPage(request), 3L, 6L, 5L, 4L);
|
||||
request.setBrokeragePriceMin(0L);
|
||||
assertPage(memberUserMapper.selectPage(request), 3L, 6L, 5L, 4L);
|
||||
request.setBrokeragePriceMin(3000000000L);
|
||||
request.setBrokeragePriceMax(3000000000L);
|
||||
assertPage(memberUserMapper.selectPage(request), 1L, 7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void incomeRangeFiltersBeforePaginationAndCombinesWithExistingConditions() {
|
||||
MemberUserPageReqVO request = new MemberUserPageReqVO();
|
||||
request.setBrokeragePriceMin(300L);
|
||||
request.setBrokeragePriceMax(300L);
|
||||
request.setPageSize(1);
|
||||
assertPage(memberUserMapper.selectPage(request), 2L, 3L);
|
||||
request.setPageNo(2);
|
||||
assertPage(memberUserMapper.selectPage(request), 2L, 1L);
|
||||
request.setPageNo(1);
|
||||
request.setWechatNum("bob");
|
||||
request.setSex(1);
|
||||
request.setStatus(0);
|
||||
request.setMemberRecentTagList(Collections.singletonList("5"));
|
||||
assertPage(memberUserMapper.selectPage(request), 1L, 3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenantInterceptorIsolatesBothMemberAndIncomeSubquery() {
|
||||
jdbcTemplate.update("UPDATE member_user SET tenant_id = 1 WHERE id = 2");
|
||||
insertIncome(1L, 1, 100000, 1, "推广佣金", "其他租户", false);
|
||||
insertIncome(4L, 1, 400, 1, "推广佣金", "其他租户", false);
|
||||
jdbcTemplate.update("UPDATE trade_brokerage_record SET tenant_id = 1 WHERE description = '其他租户'");
|
||||
List<InnerInterceptor> existing = new ArrayList<>(mybatisPlusInterceptor.getInterceptors());
|
||||
MyBatisUtils.addInterceptor(mybatisPlusInterceptor,
|
||||
new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(new TenantProperties())), 0);
|
||||
TenantContextHolder.setTenantId(0L);
|
||||
try {
|
||||
MemberUserPageReqVO request = new MemberUserPageReqVO();
|
||||
request.setBrokeragePriceMin(300L);
|
||||
request.setBrokeragePriceMax(1000L);
|
||||
assertPage(memberUserMapper.selectPage(request), 2L, 3L, 1L);
|
||||
} finally {
|
||||
mybatisPlusInterceptor.setInterceptors(existing);
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectPageBySex() {
|
||||
MemberUserPageReqVO request = new MemberUserPageReqVO();
|
||||
@@ -360,6 +440,13 @@ class MemberUserMapperTest extends BaseDbUnitTest {
|
||||
memberId, type, deleted);
|
||||
}
|
||||
|
||||
private void insertIncome(Long userId, int bizType, int price, int status,
|
||||
String title, String description, boolean deleted) {
|
||||
jdbcTemplate.update("INSERT INTO trade_brokerage_record"
|
||||
+ " (user_id, biz_type, price, status, title, description, deleted) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
userId, bizType, price, status, title, description, deleted);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exportCountAndCursorUseAllListFiltersAndIgnorePageLimits() {
|
||||
jdbcTemplate.update("UPDATE member_user SET create_time = '2026-10-01 10:00:00', login_date = '2026-10-01 10:00:00'");
|
||||
@@ -403,6 +490,14 @@ class MemberUserMapperTest extends BaseDbUnitTest {
|
||||
query.setLoginDate(new LocalDateTime[]{LocalDateTime.of(2026, 10, 21, 0, 0), LocalDateTime.of(2026, 10, 21, 23, 59)});
|
||||
queries.add(query);
|
||||
query = new MemberUserPageReqVO();
|
||||
query.setBrokeragePriceMin(300L);
|
||||
query.setBrokeragePriceMax(1000L);
|
||||
queries.add(query);
|
||||
query = new MemberUserPageReqVO();
|
||||
query.setBrokeragePriceMin(0L);
|
||||
query.setBrokeragePriceMax(0L);
|
||||
queries.add(query);
|
||||
query = new MemberUserPageReqVO();
|
||||
query.setSex(2);
|
||||
query.setStatus(1);
|
||||
query.setWechatNum("alice");
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package cn.iocoder.yudao.module.member.service.opentype;
|
||||
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
|
||||
import cn.iocoder.yudao.module.member.dal.mysql.opentype.OpenTypeUserMapper;
|
||||
import cn.iocoder.yudao.module.member.service.user.MemberUserService;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.TradeBrokerageRecordApi;
|
||||
import cn.iocoder.yudao.module.trade.api.brokerage.dto.BrokerageOrderSummaryRespDTO;
|
||||
import cn.iocoder.yudao.module.trade.enums.brokerage.BrokerageRecordBizTypeEnum;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OpenTypeUserBrokerageTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private OpenTypeServiceImpl service;
|
||||
@Mock
|
||||
private OpenTypeUserMapper mapper;
|
||||
@Mock
|
||||
private TradeBrokerageRecordApi brokerageApi;
|
||||
@Mock
|
||||
private MemberUserService memberUserService;
|
||||
|
||||
@Test
|
||||
void pageUsesStoredOrderRecipientsAndBatchesProfilesIncludingMissingMembers() {
|
||||
OpenTypeUserRespVO paid = record(100L);
|
||||
OpenTypeUserRespVO empty = record(101L);
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
Page<OpenTypeUserRespVO> page = new Page<>();
|
||||
page.setRecords(Arrays.asList(paid, empty));
|
||||
page.setTotal(2);
|
||||
when(mapper.page(any(), eq(query))).thenReturn(page);
|
||||
BrokerageOrderSummaryRespDTO summary = new BrokerageOrderSummaryRespDTO();
|
||||
summary.setSettledPrice(250L);
|
||||
summary.setPendingPrice(100L);
|
||||
summary.setCancelledPrice(300L);
|
||||
BrokerageOrderSummaryRespDTO.Recipient first = new BrokerageOrderSummaryRespDTO.Recipient();
|
||||
first.setUserId(10L);
|
||||
first.setSourceUserLevel(1);
|
||||
first.setSettledPrice(250L);
|
||||
BrokerageOrderSummaryRespDTO.Recipient missing = new BrokerageOrderSummaryRespDTO.Recipient();
|
||||
missing.setUserId(30L);
|
||||
missing.setSourceUserLevel(2);
|
||||
missing.setCancelledPrice(300L);
|
||||
summary.setRecipients(Arrays.asList(first, missing));
|
||||
when(brokerageApi.getOrderSummaryMap(BrokerageRecordBizTypeEnum.OPEN_TYPE, Arrays.asList("100", "101")))
|
||||
.thenReturn(Collections.singletonMap("100", summary));
|
||||
when(memberUserService.getUserList(any())).thenReturn(Collections.singletonList(new MemberUserDO()
|
||||
.setId(10L).setUserCode("000010").setNickname("实际收益人").setAvatar("avatar.png")));
|
||||
|
||||
assertThat(service.getOpenTypeUserPage(query).getTotal()).isEqualTo(2);
|
||||
assertThat(paid.getBrokerageSummary().getSettledPrice()).isEqualTo(250);
|
||||
assertThat(paid.getBrokerageSummary().getPendingPrice()).isEqualTo(100);
|
||||
assertThat(paid.getBrokerageSummary().getCancelledPrice()).isEqualTo(300);
|
||||
assertThat(paid.getBrokerageSummary().getRecipients()).hasSize(2);
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(0).getUserCode()).isEqualTo("000010");
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(0).getNickname()).isEqualTo("实际收益人");
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(0).isMemberExists()).isTrue();
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(1).getUserId()).isEqualTo(30);
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(1).isMemberExists()).isFalse();
|
||||
assertThat(paid.getBrokerageSummary().getRecipients().get(1).getCancelledPrice()).isEqualTo(300);
|
||||
assertThat(empty.getBrokerageSummary().getSettledPrice()).isZero();
|
||||
assertThat(empty.getBrokerageSummary().getRecipients()).isEmpty();
|
||||
verify(memberUserService).getUserList(argThat(ids -> ids.size() == 2 && ids.containsAll(Arrays.asList(10L, 30L))));
|
||||
verifyNoMoreInteractions(memberUserService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyOrdersAndNoCommissionDoNotQueryMemberProfiles() {
|
||||
service.fillBrokerageSummary(Collections.emptyList());
|
||||
verifyNoInteractions(brokerageApi, memberUserService);
|
||||
when(brokerageApi.getOrderSummaryMap(any(), any())).thenReturn(Collections.emptyMap());
|
||||
OpenTypeUserRespVO record = record(100L);
|
||||
service.fillBrokerageSummary(Collections.singletonList(record));
|
||||
assertThat(record.getBrokerageSummary().getSettledPrice()).isZero();
|
||||
assertThat(record.getBrokerageSummary().getPendingPrice()).isZero();
|
||||
assertThat(record.getBrokerageSummary().getCancelledPrice()).isZero();
|
||||
verifyNoInteractions(memberUserService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commissionQueryFailuresPropagate() {
|
||||
when(brokerageApi.getOrderSummaryMap(any(), any())).thenThrow(new IllegalStateException("查询失败"));
|
||||
assertThatThrownBy(() -> service.fillBrokerageSummary(Collections.singletonList(record(100L))))
|
||||
.isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
private OpenTypeUserRespVO record(Long id) {
|
||||
OpenTypeUserRespVO record = new OpenTypeUserRespVO();
|
||||
record.setId(id);
|
||||
record.setMemberId(99L);
|
||||
return record;
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package cn.iocoder.yudao.module.member.service.opentype;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserBrokerageRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserRespVO;
|
||||
import cn.iocoder.yudao.module.member.dal.mysql.opentype.OpenTypeUserMapper;
|
||||
import cn.iocoder.yudao.module.member.framework.export.config.MemberUserExportProperties;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OpenTypeUserExportWriterTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private OpenTypeUserExportWriter writer;
|
||||
@Mock
|
||||
private OpenTypeUserMapper mapper;
|
||||
@Mock
|
||||
private OpenTypeService openTypeService;
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@Test
|
||||
void exportsBatchesAcrossSheetsWithCurrencyTextIdsAndLegacyPaymentStatus() throws Exception {
|
||||
MemberUserExportProperties properties = new MemberUserExportProperties();
|
||||
properties.setBatchSize(2);
|
||||
properties.setRowsPerSheet(1);
|
||||
ReflectionTestUtils.setField(writer, "properties", properties);
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
OpenTypeUserRespVO first = record(3L);
|
||||
first.setPayStatus(false);
|
||||
first.setPayTime(LocalDateTime.of(2026, 11, 11, 10, 0));
|
||||
first.setPayOrderId(9007199254740993L);
|
||||
first.setRefundStatus(10);
|
||||
first.setRefundPayPrice(1234);
|
||||
OpenTypeUserRespVO second = record(2L);
|
||||
second.setPayStatus(false);
|
||||
when(mapper.selectExportUpperBound(query)).thenReturn(3L);
|
||||
when(mapper.selectExportCount(query, 3L)).thenReturn(2L);
|
||||
when(mapper.selectExportBatch(query, 3L, null, null, 2)).thenReturn(Arrays.asList(first, second));
|
||||
when(mapper.selectExportBatch(query, 3L, second.getCreateTime(), 2L, 2)).thenReturn(Collections.emptyList());
|
||||
doAnswer(invocation -> {
|
||||
OpenTypeUserBrokerageRespVO summary = new OpenTypeUserBrokerageRespVO();
|
||||
summary.setSettledPrice(250L);
|
||||
summary.setPendingPrice(100L);
|
||||
summary.setCancelledPrice(300L);
|
||||
OpenTypeUserBrokerageRespVO.Recipient recipient = new OpenTypeUserBrokerageRespVO.Recipient();
|
||||
recipient.setUserId(10L);
|
||||
recipient.setUserCode("000010");
|
||||
recipient.setNickname("分销人");
|
||||
recipient.setMemberExists(true);
|
||||
recipient.setSourceUserLevel(1);
|
||||
recipient.setSettledPrice(250L);
|
||||
recipient.setPendingPrice(100L);
|
||||
recipient.setCancelledPrice(300L);
|
||||
summary.setRecipients(Collections.singletonList(recipient));
|
||||
first.setBrokerageSummary(summary);
|
||||
return null;
|
||||
}).when(openTypeService).fillBrokerageSummary(anyList());
|
||||
List<Long> progress = new ArrayList<>();
|
||||
Path file = directory.resolve("history.xlsx");
|
||||
assertThat(writer.write(file, query, (total, count) -> progress.add(count))).isEqualTo(2);
|
||||
assertThat(progress).containsExactly(0L, 2L);
|
||||
try (Workbook workbook = WorkbookFactory.create(file.toFile())) {
|
||||
assertThat(workbook.getNumberOfSheets()).isEqualTo(2);
|
||||
Row row = workbook.getSheetAt(0).getRow(1);
|
||||
assertThat(row.getCell(1).getStringCellValue()).isEqualTo("000001");
|
||||
assertThat(row.getCell(2).getStringCellValue()).isEqualTo("=1+1");
|
||||
assertThat(row.getCell(2).getCellType()).isEqualTo(CellType.STRING);
|
||||
assertThat(row.getCell(3).getStringCellValue()).isEqualTo("0013900000000");
|
||||
assertThat(row.getCell(6).getNumericCellValue()).isEqualTo(12.34);
|
||||
assertThat(row.getCell(7).getStringCellValue()).isEqualTo("支付成功");
|
||||
assertThat(row.getCell(9).getStringCellValue()).isEqualTo("9007199254740993");
|
||||
assertThat(row.getCell(11).getStringCellValue()).isEqualTo("退款成功");
|
||||
assertThat(row.getCell(12).getNumericCellValue()).isEqualTo(12.34);
|
||||
assertThat(new DataFormatter().formatCellValue(row.getCell(14))).isEqualTo("2026-11-11 09:00:00");
|
||||
assertThat(workbook.getSheetAt(1).getRow(1).getCell(7).getStringCellValue()).isEqualTo("未支付");
|
||||
assertThat(workbook.getSheetAt(1).getRow(1).getCell(11).getStringCellValue()).isEqualTo("未退款");
|
||||
assertThat(row.getCell(15).getNumericCellValue()).isEqualTo(2.50);
|
||||
assertThat(row.getCell(16).getNumericCellValue()).isEqualTo(1.00);
|
||||
assertThat(row.getCell(17).getNumericCellValue()).isEqualTo(3.00);
|
||||
assertThat(new DataFormatter().formatCellValue(row.getCell(15))).isEqualTo("2.50");
|
||||
assertThat(new DataFormatter().formatCellValue(row.getCell(16))).isEqualTo("1.00");
|
||||
assertThat(new DataFormatter().formatCellValue(row.getCell(17))).isEqualTo("3.00");
|
||||
assertThat(row.getCell(18).getStringCellValue()).contains("1级分销", "分销人", "ID 000010",
|
||||
"已结算 2.50元", "待结算 1.00元", "已取消 3.00元");
|
||||
for (Sheet sheet : workbook) {
|
||||
assertThat(sheet.getColumnWidth(18)).isEqualTo(200 * 256);
|
||||
for (Row exportedRow : sheet) {
|
||||
assertThat(exportedRow.getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
}
|
||||
for (Cell cell : sheet.getRow(0)) {
|
||||
assertThat(cell.getCellStyle().getAlignment()).isEqualTo(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
assertThat(cell.getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
}
|
||||
}
|
||||
assertThat(row.getCell(0).getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
}
|
||||
verify(openTypeService).fillBrokerageSummary(Arrays.asList(first, second));
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptySearchStillProducesColumnHeaders() throws Exception {
|
||||
ReflectionTestUtils.setField(writer, "properties", new MemberUserExportProperties());
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
when(mapper.selectExportUpperBound(query)).thenReturn(null);
|
||||
Path file = directory.resolve("empty.xlsx");
|
||||
assertThat(writer.write(file, query, (total, count) -> assertThat(count).isZero())).isZero();
|
||||
try (Workbook workbook = WorkbookFactory.create(file.toFile())) {
|
||||
assertThat(workbook.getSheetAt(0).getLastRowNum()).isZero();
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue()).isEqualTo("记录编号");
|
||||
assertThat(workbook.getSheetAt(0).getColumnWidth(18)).isEqualTo(200 * 256);
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getCell(0).getCellStyle().getAlignment())
|
||||
.isEqualTo(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
}
|
||||
verify(mapper, never()).selectExportBatch(any(), any(), any(), any(), anyInt());
|
||||
verifyNoInteractions(openTypeService);
|
||||
}
|
||||
|
||||
private OpenTypeUserRespVO record(Long id) {
|
||||
OpenTypeUserRespVO record = new OpenTypeUserRespVO();
|
||||
record.setId(id);
|
||||
record.setMemberUserCode("000001");
|
||||
record.setMemberNickname("=1+1");
|
||||
record.setMemberMobile("0013900000000");
|
||||
record.setVipTitle("月卡");
|
||||
record.setVipDuration("30");
|
||||
record.setPayPrice(1234);
|
||||
record.setCreateTime(LocalDateTime.of(2026, 11, 11, 9, 0));
|
||||
return record;
|
||||
}
|
||||
}
|
||||
+34
-3
@@ -5,11 +5,13 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.opentype.vo.openTypeUser.OpenTypeUserReqVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserExportRespVO;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
import cn.iocoder.yudao.module.member.dal.redis.user.MemberUserExportRedisDAO;
|
||||
import cn.iocoder.yudao.module.member.framework.export.config.MemberUserExportProperties;
|
||||
import cn.iocoder.yudao.module.member.service.user.BO.MemberUserExportTask;
|
||||
import cn.iocoder.yudao.module.member.service.opentype.OpenTypeUserExportWriter;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -40,6 +42,8 @@ class MemberUserExportServiceTest extends BaseMockitoUnitTest {
|
||||
private MemberUserExportRedisDAO exportRedisDAO;
|
||||
@Mock
|
||||
private MemberUserExportWriter exportWriter;
|
||||
@Mock
|
||||
private OpenTypeUserExportWriter historyExportWriter;
|
||||
@TempDir
|
||||
Path directory;
|
||||
|
||||
@@ -54,7 +58,7 @@ class MemberUserExportServiceTest extends BaseMockitoUnitTest {
|
||||
properties.setDirectory(directory.toString());
|
||||
ReflectionTestUtils.setField(service, "properties", properties);
|
||||
ReflectionTestUtils.setField(service, "executor", (Executor) queue::add);
|
||||
lenient().when(exportRedisDAO.acquire(any(), any(), anyString())).thenReturn(true);
|
||||
lenient().when(exportRedisDAO.acquire(any(), any(), anyString(), nullable(String.class))).thenReturn(true);
|
||||
lenient().doAnswer(invocation -> {
|
||||
MemberUserExportTask task = invocation.getArgument(0);
|
||||
task.setUpdatedAt(System.currentTimeMillis());
|
||||
@@ -147,8 +151,8 @@ class MemberUserExportServiceTest extends BaseMockitoUnitTest {
|
||||
@Test
|
||||
void duplicateSubmissionsReturnExistingTaskWithoutQueuingAnotherExport() {
|
||||
String taskId = service.create(new MemberUserPageReqVO()).getTaskId();
|
||||
when(exportRedisDAO.acquire(any(), any(), anyString())).thenReturn(false);
|
||||
when(exportRedisDAO.getActiveTaskId(1L, 100L)).thenReturn(taskId);
|
||||
when(exportRedisDAO.acquire(any(), any(), anyString(), isNull())).thenReturn(false);
|
||||
when(exportRedisDAO.getActiveTaskId(1L, 100L, null)).thenReturn(taskId);
|
||||
|
||||
MemberUserExportRespVO duplicate = service.create(new MemberUserPageReqVO());
|
||||
assertThat(duplicate.getTaskId()).isEqualTo(taskId);
|
||||
@@ -197,6 +201,33 @@ class MemberUserExportServiceTest extends BaseMockitoUnitTest {
|
||||
assertThat(expiredImages).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void historyUsesItsOwnWriterAndTaskTypeAndCannotBeDownloadedThroughUserEndpoints() throws Exception {
|
||||
OpenTypeUserReqVO query = new OpenTypeUserReqVO();
|
||||
query.setPayStatus(false);
|
||||
when(historyExportWriter.write(any(), eq(query), any())).thenAnswer(invocation -> {
|
||||
Files.write(invocation.getArgument(0), new byte[]{4, 5, 6});
|
||||
return 2L;
|
||||
});
|
||||
String userTaskId = service.create(new MemberUserPageReqVO()).getTaskId();
|
||||
MemberUserExportRespVO submitted = service.createHistory(query);
|
||||
assertThat(submitted.getFileName()).startsWith("会员开通记录_");
|
||||
assertThat(queue).hasSize(2);
|
||||
assertThatThrownBy(() -> service.getProgress(submitted.getTaskId())).isInstanceOf(ServiceException.class);
|
||||
assertThatThrownBy(() -> service.getHistoryProgress(userTaskId)).isInstanceOf(ServiceException.class);
|
||||
assertThatThrownBy(() -> service.getHistoryDownloadFile(userTaskId)).isInstanceOf(ServiceException.class);
|
||||
queue.remove(1).run();
|
||||
assertThat(service.getHistoryProgress(submitted.getTaskId()).getStatus()).isEqualTo("SUCCESS");
|
||||
assertThat(Files.readAllBytes(service.getHistoryDownloadFile(submitted.getTaskId()))).containsExactly(4, 5, 6);
|
||||
assertThatThrownBy(() -> service.getDownloadFile(submitted.getTaskId())).isInstanceOf(ServiceException.class);
|
||||
verifyNoInteractions(exportWriter);
|
||||
verify(exportRedisDAO).acquire(1L, 100L, submitted.getTaskId(), "open-type-history");
|
||||
login(200L, 1L);
|
||||
assertThatThrownBy(() -> service.getHistoryProgress(submitted.getTaskId())).isInstanceOf(ServiceException.class);
|
||||
login(100L, 2L);
|
||||
assertThatThrownBy(() -> service.getHistoryDownloadFile(submitted.getTaskId())).isInstanceOf(ServiceException.class);
|
||||
}
|
||||
|
||||
private void login(Long userId, Long tenantId) {
|
||||
LoginUser user = new LoginUser();
|
||||
user.setId(userId);
|
||||
|
||||
+14
-1
@@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.member.service.user;
|
||||
|
||||
import cn.iocoder.yudao.framework.excel.core.style.ExcelExportStyleTokens;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO;
|
||||
import cn.iocoder.yudao.module.member.dal.dataobject.address.MemberAddressDO;
|
||||
@@ -112,7 +113,16 @@ class MemberUserExportWriterTest extends BaseMockitoUnitTest {
|
||||
assertThat(new DataFormatter().formatCellValue(row.getCell(8))).isEqualTo("2026-10-30 10:20:30");
|
||||
assertThat(workbook.getSheetAt(1).getRow(1).getCell(0).getStringCellValue()).isEqualTo("u2");
|
||||
assertThat(workbook.getSheetAt(1).getRow(2).getCell(0).getStringCellValue()).isEqualTo("u1");
|
||||
assertThat(row.getHeightInPoints()).isEqualTo(24);
|
||||
for (Sheet sheet : workbook) {
|
||||
for (Row exportedRow : sheet) {
|
||||
assertThat(exportedRow.getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
}
|
||||
for (Cell cell : sheet.getRow(0)) {
|
||||
assertThat(cell.getCellStyle().getAlignment()).isEqualTo(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
assertThat(cell.getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
}
|
||||
}
|
||||
assertThat(row.getCell(0).getCellStyle().getVerticalAlignment()).isEqualTo(ExcelExportStyleTokens.VERTICAL_ALIGNMENT);
|
||||
assertThat(workbook.getAllPictures()).isEmpty();
|
||||
}
|
||||
verify(addressService, times(3)).getAddressListByUserIds(any());
|
||||
@@ -133,6 +143,9 @@ class MemberUserExportWriterTest extends BaseMockitoUnitTest {
|
||||
try (Workbook workbook = WorkbookFactory.create(file.toFile())) {
|
||||
assertThat(workbook.getSheetAt(0).getLastRowNum()).isZero();
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue()).isEqualTo("用户ID");
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getHeightInPoints()).isEqualTo(ExcelExportStyleTokens.ROW_HEIGHT);
|
||||
assertThat(workbook.getSheetAt(0).getRow(0).getCell(0).getCellStyle().getAlignment())
|
||||
.isEqualTo(ExcelExportStyleTokens.HEAD_HORIZONTAL_ALIGNMENT);
|
||||
}
|
||||
verify(memberUserMapper, never()).selectExportBatch(any(), anyLong(), any(), anyInt());
|
||||
verifyNoInteractions(addressService, memberRecentTagMapper);
|
||||
|
||||
+12
@@ -19,3 +19,15 @@ CREATE TABLE IF NOT EXISTS member_recent_tag (
|
||||
tag_biz_type varchar(32),
|
||||
deleted bit NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trade_brokerage_record (
|
||||
id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
biz_type int NOT NULL,
|
||||
price int NOT NULL,
|
||||
status int NOT NULL,
|
||||
title varchar(64),
|
||||
description varchar(500),
|
||||
deleted bit NOT NULL DEFAULT FALSE,
|
||||
tenant_id bigint NOT NULL DEFAULT 0
|
||||
);
|
||||
Reference in new issue
Block a user