增加订单导出

This commit is contained in:
hy001 2026-06-09 15:43:05 +08:00
parent c8e9c8c257
commit c7016d5ae4
9 changed files with 368 additions and 96 deletions

View File

@ -98,8 +98,13 @@ public class InAppPurchaseDetailsQryCmd extends Command {
*/
private String lastId;
/**
* 条数.
*/
private Integer limit;
}
/**
* 条数.
*/
private Integer limit;
/**
* 是否导出全部筛选结果导出时不使用 lastId limit 分页.
*/
private Boolean exportAll;
}

View File

@ -1,12 +1,15 @@
package com.red.circle.console.adapter.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.app.service.app.order.InAppPurchaseBackService;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.app.service.app.order.InAppPurchaseBackService;
import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -45,9 +48,19 @@ public class InAppPurchaseRestController extends BaseController {
/**
* 订单统计查询 - 返回金额总数和订单总数
*/
@GetMapping("/statistics")
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return inAppPurchaseBackService.statistics(query);
}
}
@GetMapping("/statistics")
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return inAppPurchaseBackService.statistics(query);
}
/**
* 订单详情导出.
*/
@OpsOperationReqLog(value = "导出-订单详情", ignoreRequestParam = true)
@GetMapping("/details/export")
public void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException {
inAppPurchaseBackService.exportDetails(response, query);
}
}

View File

@ -0,0 +1,88 @@
package com.red.circle.console.app.excel.model.order;
import com.alibaba.excel.annotation.ExcelProperty;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 内购订单详情导出模型.
*/
@Data
@Accessors(chain = true)
public class InAppPurchaseDetailsExportModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "Env")
private String env;
@ExcelProperty(value = "Sys Origin")
private String sysOrigin;
@ExcelProperty(value = "Platform")
private String platform;
@ExcelProperty(value = "Factory")
private String factoryCode;
@ExcelProperty(value = "Channel")
private String channel;
@ExcelProperty(value = "Buyer User ID")
private String buyerUserId;
@ExcelProperty(value = "Buyer Account")
private String buyerAccount;
@ExcelProperty(value = "Buyer Nickname")
private String buyerNickname;
@ExcelProperty(value = "Accept User ID")
private String acceptUserId;
@ExcelProperty(value = "Accept Account")
private String acceptAccount;
@ExcelProperty(value = "Accept Nickname")
private String acceptNickname;
@ExcelProperty(value = "Amount(USD)")
private BigDecimal amountUsd;
@ExcelProperty(value = "Amount")
private BigDecimal amount;
@ExcelProperty(value = "Currency")
private String currency;
@ExcelProperty(value = "Description")
private String productDescriptor;
@ExcelProperty(value = "Status")
private String status;
@ExcelProperty(value = "Receipt Type")
private String receiptType;
@ExcelProperty(value = "Third Party Order ID")
private String orderId;
@ExcelProperty(value = "App Order ID")
private String appOrderId;
@ExcelProperty(value = "Track ID")
private String trackId;
@ExcelProperty(value = "Country Code")
private String countryCode;
@ExcelProperty(value = "Create Time")
private String createTime;
@ExcelProperty(value = "Update Time")
private String updateTime;
}

View File

@ -1,21 +1,30 @@
package com.red.circle.console.app.service.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.infra.database.rds.entity.admin.User;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.order.inner.endpoint.InAppPurchaseDetailsClient;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
package com.red.circle.console.app.service.app.order;
import com.alibaba.excel.EasyExcel;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.console.app.excel.EasyExcelUtils;
import com.red.circle.console.app.excel.model.order.InAppPurchaseDetailsExportModel;
import com.red.circle.console.infra.database.rds.entity.admin.User;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.order.inner.endpoint.InAppPurchaseDetailsClient;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseFactoryDTO;
import com.red.circle.order.inner.model.dto.inapp.InAppPurchaseDetailsDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -126,11 +135,92 @@ public class InAppPurchaseBackServiceImpl implements InAppPurchaseBackService {
}).collect(Collectors.toList());
}
@Override
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return ResponseAssert.requiredSuccess(
inAppPurchaseDetailsClient.statistics(query)
);
}
}
@Override
public Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query) {
return ResponseAssert.requiredSuccess(
inAppPurchaseDetailsClient.statistics(query)
);
}
@Override
public void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException {
query.setLastId(null);
query.setLimit(null);
query.setExportAll(Boolean.TRUE);
List<InAppPurchaseDetailsExportModel> exportRows = listDetails(query).stream()
.map(this::toExportModel)
.collect(Collectors.toList());
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String fileName = URLEncoder.encode("InAppPurchaseOrders", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), InAppPurchaseDetailsExportModel.class)
.registerWriteHandler(EasyExcelUtils.getStyleStrategy())
.sheet("Orders")
.doWrite(exportRows);
}
private InAppPurchaseDetailsExportModel toExportModel(InAppPurchaseDetailsCO co) {
InAppPurchaseDetailsDTO details = co.getDetails();
InAppPurchaseFactoryDTO factory = Objects.isNull(details) ? null : details.getFactory();
UserProfileDTO buyer = pickBuyerProfile(co, details);
UserProfileDTO accept = co.getAcceptUserProfile();
return new InAppPurchaseDetailsExportModel()
.setEnv(text(Objects.isNull(details) ? null : details.getEnv()))
.setSysOrigin(text(Objects.isNull(details) ? null : details.getSysOrigin()))
.setPlatform(text(Objects.isNull(factory) ? null : factory.getPlatform()))
.setFactoryCode(text(Objects.isNull(factory) ? null : factory.getFactoryCode()))
.setChannel(text(Objects.isNull(factory) ? null : factory.getFactoryChannelCode()))
.setBuyerUserId(userId(buyer, Objects.isNull(details) ? null : details.getCreateUser()))
.setBuyerAccount(text(Objects.isNull(buyer) ? null : buyer.getAccount()))
.setBuyerNickname(buyerNickname(co, buyer))
.setAcceptUserId(userId(accept, Objects.isNull(details) ? null : details.getAcceptUserId()))
.setAcceptAccount(text(Objects.isNull(accept) ? null : accept.getAccount()))
.setAcceptNickname(text(Objects.isNull(accept) ? null : accept.getUserNickname()))
.setAmountUsd(Objects.isNull(details) ? null : details.getAmountUsd())
.setAmount(Objects.isNull(details) ? null : details.getAmount())
.setCurrency(text(Objects.isNull(details) ? null : details.getCurrency()))
.setProductDescriptor(text(Objects.isNull(details) ? null : details.getProductDescriptor()))
.setStatus(text(Objects.isNull(details) ? null : details.getStatus()))
.setReceiptType(text(Objects.isNull(details) ? null : details.getReceiptType()))
.setOrderId(text(Objects.isNull(details) ? null : details.getOrderId()))
.setAppOrderId(text(Objects.isNull(details) ? null : details.getId()))
.setTrackId(text(Objects.isNull(details) ? null : details.getTrackId()))
.setCountryCode(text(Objects.isNull(details) ? null : details.getCountryCode()))
.setCreateTime(text(Objects.isNull(details) ? null : details.getCreateTime()))
.setUpdateTime(text(Objects.isNull(details) ? null : details.getUpdateTime()));
}
private UserProfileDTO pickBuyerProfile(InAppPurchaseDetailsCO co, InAppPurchaseDetailsDTO details) {
if (Objects.nonNull(co.getCreateUserProfile())) {
return co.getCreateUserProfile();
}
if (Objects.nonNull(details) && Objects.equals(details.getCreateUser(), details.getAcceptUserId())) {
return co.getAcceptUserProfile();
}
return null;
}
private String buyerNickname(InAppPurchaseDetailsCO co, UserProfileDTO buyer) {
if (StringUtils.isNotBlank(co.getCreateNickname())) {
return co.getCreateNickname();
}
return text(Objects.isNull(buyer) ? null : buyer.getUserNickname());
}
private String userId(UserProfileDTO profile, Long fallback) {
if (Objects.nonNull(profile) && Objects.nonNull(profile.getId())) {
return Objects.toString(profile.getId());
}
return text(fallback);
}
private String text(Object value) {
return Objects.isNull(value) ? "" : Objects.toString(value);
}
}

View File

@ -1,9 +1,11 @@
package com.red.circle.console.app.service.app.order;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import java.util.List;
import java.util.Map;
import com.red.circle.console.app.dto.clienobject.order.InAppPurchaseDetailsCO;
import com.red.circle.order.inner.model.cmd.inapp.InAppPurchaseDetailsQryCmd;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 内购服务.
@ -25,6 +27,12 @@ public interface InAppPurchaseBackService extends IOrderService {
/**
* 订单统计查询 - 返回金额总数和订单总数
*/
Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query);
}
Map<String, Object> statistics(InAppPurchaseDetailsQryCmd query);
/**
* 按筛选条件导出订单详情不使用分页条件.
*/
void exportDetails(HttpServletResponse response, InAppPurchaseDetailsQryCmd query)
throws IOException;
}

View File

@ -153,10 +153,11 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
).getModifiedCount() > 0;
}
@Override
public List<InAppPurchaseDetails> list(InAppPurchaseDetailsQuery query) {
@Override
public List<InAppPurchaseDetails> list(InAppPurchaseDetailsQuery query) {
Criteria criteria = new Criteria();
boolean exportAll = Boolean.TRUE.equals(query.getExportAll());
if (StringUtils.isNotBlank(query.getId())) {
query.setLastId(null);
@ -205,18 +206,20 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
);
}
if (StringUtils.isNotBlank(query.getLastId())) {
if (!exportAll && StringUtils.isNotBlank(query.getLastId())) {
criteria.and(ID_FIELD).lt(query.getLastId());
}
if (Objects.isNull(query.getLimit()) || query.getLimit() <= 0) {
query.setLimit(PageConstant.DEFAULT_LIMIT_SIZE);
}
return mongoTemplate
.find(Query.query(criteria)
.with(Sort.by(Sort.Order.desc(ID_FIELD)))
.limit(query.getLimit()), InAppPurchaseDetails.class);
Query mongoQuery = Query.query(criteria)
.with(Sort.by(Sort.Order.desc(ID_FIELD)));
if (!exportAll) {
if (Objects.isNull(query.getLimit()) || query.getLimit() <= 0) {
query.setLimit(PageConstant.DEFAULT_LIMIT_SIZE);
}
mongoQuery.limit(query.getLimit());
}
return mongoTemplate.find(mongoQuery, InAppPurchaseDetails.class);
}
@Override

View File

@ -96,8 +96,13 @@ public class InAppPurchaseDetailsQuery implements Serializable {
*/
private String lastId;
/**
* 条数.
*/
private Integer limit;
}
/**
* 条数.
*/
private Integer limit;
/**
* 是否导出全部筛选结果导出时不使用 lastId limit 分页.
*/
private Boolean exportAll;
}

View File

@ -63,17 +63,21 @@ public class InAppPurchaseDetailsClientServiceImpl implements InAppPurchaseDetai
@Override
public List<InAppPurchaseDetailsDTO> list(InAppPurchaseDetailsQryCmd qryCmd) {
boolean exportAll = Boolean.TRUE.equals(qryCmd.getExportAll());
List<InAppPurchaseDetailsDTO> mongoOrders = safeList(
inAppPurchaseDetailsInnerConvert.toListInAppPurchaseDetailsDTO(
inAppPurchaseDetailsService.list(
inAppPurchaseDetailsInnerConvert.toInAppPurchaseDetailsQuery(qryCmd))
));
List<InAppPurchaseDetailsDTO> miFaPayOrders = listMiFaPayOrders(qryCmd, true).stream()
List<InAppPurchaseDetailsDTO> miFaPayOrders = listMiFaPayOrders(qryCmd, !exportAll).stream()
.map(order -> inAppPurchaseDetailsInnerConvert.toInAppPurchaseDetailsDTO(
toMiFaPayDetails(order, false)))
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (exportAll) {
return mergeAll(mongoOrders, miFaPayOrders);
}
return mergePage(mongoOrders, miFaPayOrders, normalizeLimit(qryCmd.getLimit()));
}
@ -230,6 +234,13 @@ public class InAppPurchaseDetailsClientServiceImpl implements InAppPurchaseDetai
private List<InAppPurchaseDetailsDTO> mergePage(List<InAppPurchaseDetailsDTO> first,
List<InAppPurchaseDetailsDTO> second, int limit) {
return mergeAll(first, second).stream()
.limit(limit)
.collect(Collectors.toList());
}
private List<InAppPurchaseDetailsDTO> mergeAll(List<InAppPurchaseDetailsDTO> first,
List<InAppPurchaseDetailsDTO> second) {
List<InAppPurchaseDetailsDTO> result = new ArrayList<>();
result.addAll(first);
result.addAll(second);
@ -237,7 +248,6 @@ public class InAppPurchaseDetailsClientServiceImpl implements InAppPurchaseDetai
.filter(Objects::nonNull)
.sorted(Comparator.comparing(InAppPurchaseDetailsDTO::getId,
this::compareIdDesc))
.limit(limit)
.collect(Collectors.toList());
}

View File

@ -5,12 +5,13 @@ import com.google.common.collect.Lists;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.other.app.convertor.sys.SysConfigAppConvertor;
import com.red.circle.other.app.dto.clientobject.sys.GameListConfigCO;
import com.red.circle.other.app.dto.cmd.game.GameListQryCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.cache.service.other.GameListCacheService;
import com.red.circle.other.app.dto.cmd.game.GameListQryCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.UserConsumptionLevel;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.cache.service.other.GameListCacheService;
import com.red.circle.other.infra.database.rds.service.live.RoomMemberService;
import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService;
import com.red.circle.other.infra.database.rds.service.sys.VersionManageService;
@ -44,10 +45,13 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class GameListConfigQryExe {
private static final String COMMON = "COMMON";
private static final String IOS_GAME_USER_WHITE_LIST = "IOS_GAME_USER_WHITE_LIST";
private final UserRegionGateway userRegionGateway;
private final UserProfileGateway userProfileGateway;
private static final String COMMON = "COMMON";
private static final String IOS_GAME_USER_WHITE_LIST = "IOS_GAME_USER_WHITE_LIST";
private static final String GAME_LIST_TARGET_SYS_ORIGIN = "LIKEI";
private static final String GAME_LIST_TARGET_COUNTRY_CODE = "TR";
private static final int GAME_LIST_MIN_WEALTH_LEVEL = 2;
private final UserRegionGateway userRegionGateway;
private final UserProfileGateway userProfileGateway;
private final EnumConfigCacheService enumConfigCacheService;
private final SysConfigAppConvertor sysConfigAppConvertor;
private final GameListCacheService gameListCacheService;
@ -63,21 +67,67 @@ public class GameListConfigQryExe {
return configs;
}
if (StringUtils.isBlank(cmd.getRoomSessionId())) {
configs = configs.stream().filter(e -> !"CHAT_ROOM".equals(e.getGameMode())).toList();
}
// 获得用户区域
String regionId = userRegionGateway.getRegionId(cmd.requiredReqUserId());
return configs.stream()
.filter(configCO ->
StringUtils.isBlank(configCO.getRegions()) || configCO.getRegions()
.contains(regionId)
).toList();
}
private List<GameListConfigCO> isBarrageGame(AppExtCommand cmd, List<GameListConfigCO> configs) {
if (StringUtils.isBlank(cmd.getRoomSessionId())) {
configs = configs.stream().filter(e -> !"CHAT_ROOM".equals(e.getGameMode())).toList();
}
if (!canShowGameList(cmd)) {
return Lists.newArrayList();
}
// 获得用户区域配置为空代表全区域配置有值时按逗号拆分后精确匹配区域 ID
String regionId = userRegionGateway.getRegionId(cmd.requiredReqUserId());
return configs.stream()
.filter(configCO -> matchRegion(configCO.getRegions(), regionId))
.toList();
}
private boolean canShowGameList(GameListQryCmd cmd) {
if (!GAME_LIST_TARGET_SYS_ORIGIN.equalsIgnoreCase(cmd.requireReqSysOrigin())) {
return true;
}
// LIKEI 游戏列表需要同时满足用户区域财富等级和当前 IP 国家任一条件不满足直接返回空列表
Long userId = cmd.requiredReqUserId();
if (!GAME_LIST_TARGET_COUNTRY_CODE.equalsIgnoreCase(userRegionGateway.getRegionCode(userId))) {
return false;
}
UserConsumptionLevel level = userProfileGateway.getUserConsumptionLevel(
cmd.requireReqSysOriginEnum(), userId);
int wealthLevel = Optional.ofNullable(level)
.map(UserConsumptionLevel::getWealthLevel)
.orElse(0);
if (wealthLevel < GAME_LIST_MIN_WEALTH_LEVEL) {
return false;
}
return GAME_LIST_TARGET_COUNTRY_CODE.equalsIgnoreCase(ipCountryUtils.getCountryCode(resolveClientIp(cmd)));
}
private boolean matchRegion(String regions, String regionId) {
if (StringUtils.isBlank(regions)) {
return true;
}
if (StringUtils.isBlank(regionId)) {
return false;
}
return Arrays.stream(regions.split(","))
.map(String::trim)
.anyMatch(regionId::equals);
}
private String resolveClientIp(GameListQryCmd cmd) {
if (StringUtils.isNotBlank(cmd.getReqIp())) {
return cmd.getReqIp();
}
return Optional.ofNullable(latestMobileDeviceService.getByUserId(cmd.requiredReqUserId()))
.map(LatestMobileDevice::getIp)
.orElse(null);
}
private List<GameListConfigCO> isBarrageGame(AppExtCommand cmd, List<GameListConfigCO> configs) {
// 是否弹幕游戏
configs.forEach(
config -> {