构造属性结构

This commit is contained in:
2026-05-30 10:11:09 +08:00
parent 545c86098e
commit 4fb389ea3a
5 changed files with 228 additions and 101 deletions

View File

@@ -0,0 +1,18 @@
package com.rj.dto;
import lombok.Data;
import java.math.BigDecimal;
/**
* 买家在 lb_order_row 中的最新已支付订单统计。
*/
@Data
public class LbBuyerTradeStats {
private Long buyerId;
private String payTime;
private BigDecimal totalMoney;
}

View File

@@ -2,6 +2,7 @@ package com.rj.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.dto.LbBuyerTradeStats;
import com.rj.entity.LbOrderRow;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -16,4 +17,10 @@ public interface LbOrderRowMapper extends BaseMapper<LbOrderRow> {
*/
@InterceptorIgnore(tenantLine = "true")
int upsertBatch(@Param("list") List<LbOrderRow> list);
/**
* 按租户与买家汇总:每位买家取 pay_time 最新(相同时取 id 最大)的已支付订单。
*/
List<LbBuyerTradeStats> selectLatestPaidStatsByTenantId(@Param("tenantId") String tenantId,
@Param("dataType") String dataType);
}

View File

@@ -41,7 +41,8 @@ public interface ILbDepartmentUserService extends IService<LbDepartmentUser> {
Map<String, Object> syncFromLbUser(String tenantId);
/**
* 按租户查询部门用户并构建父子树parent_id 对应 lb_user.pid
* 按租户从 lb_user 构建父子树pid 为上级用户 ID
* is_active / last_trade_date / last_buy_amt 从 lb_order_row 汇总读取。
*/
Map<String, Object> getDepartmentUserTree(String tenantId, Integer isActive, LocalDate joinDate, LocalDate lastTradeDate);

View File

@@ -3,6 +3,7 @@ package com.rj.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbBuyerTradeStats;
import com.rj.entity.LbDailyUserTrade;
import com.rj.entity.LbDepartmentUser;
import com.rj.entity.LbOrderRow;
@@ -492,27 +493,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
}
}
Set<String> tradeActiveUserIds = new HashSet<>();
Map<String, LbOrderRow> latestOrderByBuyerId = new HashMap<>();
List<LbOrderRow> orderRows = lbOrderRowMapper.selectList(
new LambdaQueryWrapper<LbOrderRow>()
.eq(LbOrderRow::getTenantId, tenantIdTrim)
.eq(LbOrderRow::getDataType, ORDER_DATA_TYPE_DETAIL)
.isNotNull(LbOrderRow::getBuyerId)
.isNotNull(LbOrderRow::getPayTime));
if (orderRows != null) {
for (LbOrderRow row : orderRows) {
if (!isPaidOrderRow(row)) {
continue;
}
String buyerUserId = String.valueOf(row.getBuyerId());
tradeActiveUserIds.add(buyerUserId);
LbOrderRow prev = latestOrderByBuyerId.get(buyerUserId);
if (prev == null || isOrderPayTimeNewer(row, prev)) {
latestOrderByBuyerId.put(buyerUserId, row);
}
}
}
Map<String, LbBuyerTradeStats> tradeStatsByUserId = loadBuyerTradeStatsFromOrderRow(tenantIdTrim);
List<LbDepartmentUser> toInsert = new ArrayList<>();
List<LbDepartmentUser> toUpdate = new ArrayList<>();
@@ -556,16 +537,17 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
entity.setOrgChain(buildOrgChain(lbUser.getId(), userById));
LbOrderRow latestOrder = latestOrderByBuyerId.get(userId);
if (latestOrder != null) {
entity.setLastTradeDate(parseOrderTimeToDate(latestOrder.getPayTime()));
entity.setLastBuyAmt(latestOrder.getTotalMoney());
LbBuyerTradeStats tradeStats = tradeStatsByUserId.get(userId);
if (tradeStats != null) {
entity.setLastTradeDate(parseOrderTimeToDate(tradeStats.getPayTime()));
entity.setLastBuyAmt(tradeStats.getTotalMoney());
entity.setIsActive(1);
} else {
entity.setLastTradeDate(null);
entity.setLastBuyAmt(null);
entity.setIsActive(0);
}
entity.setUpdateTime(now);
entity.setIsActive(tradeActiveUserIds.contains(userId) ? 1 : 0);
if (isInsert) {
toInsert.add(entity);
@@ -654,7 +636,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
.replace(ORG_CHAIN_NODE_SEPARATOR, "");
}
private String resolveDisplayName(LbUser user) {
private static String resolveDisplayName(LbUser user) {
if (user.getNickname() != null && !user.getNickname().trim().isEmpty()) {
return user.getNickname().trim();
}
@@ -675,19 +657,9 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
}
String tenantIdTrim = tenantId.trim();
LambdaQueryWrapper<LbDepartmentUser> queryWrapper = new LambdaQueryWrapper<LbDepartmentUser>()
.eq(LbDepartmentUser::getTenantId, tenantIdTrim);
if (isActive != null) {
queryWrapper.eq(LbDepartmentUser::getIsActive, isActive);
}
if (joinDate != null) {
queryWrapper.gt(LbDepartmentUser::getJoinDate, joinDate);
}
if (lastTradeDate != null) {
queryWrapper.gt(LbDepartmentUser::getLastTradeDate, lastTradeDate);
}
List<LbDepartmentUser> deptUsers = this.list(queryWrapper);
if (deptUsers == null || deptUsers.isEmpty()) {
List<LbUser> lbUsers = lbUserMapper.selectList(
new LambdaQueryWrapper<LbUser>().eq(LbUser::getTenantId, tenantIdTrim));
if (lbUsers == null || lbUsers.isEmpty()) {
result.put("success", true);
result.put("message", "查询成功");
result.put("data", new ArrayList<>());
@@ -696,40 +668,67 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
return result;
}
Map<String, LbDepartmentUser> userRecordMap = new LinkedHashMap<>();
for (LbDepartmentUser row : deptUsers) {
String uid = trimToNull(row.getUserId());
if (uid == null) {
Map<String, LbUser> userById = new LinkedHashMap<>();
for (LbUser lbUser : lbUsers) {
if (lbUser.getId() != null) {
userById.put(String.valueOf(lbUser.getId()), lbUser);
}
}
Map<String, LbBuyerTradeStats> tradeStatsByUserId = loadBuyerTradeStatsFromOrderRow(tenantIdTrim);
boolean hasFilter = isActive != null || joinDate != null || lastTradeDate != null;
Set<String> matchedUserIds = new HashSet<>();
Map<String, LocalDate> joinDateByUserId = new HashMap<>();
Map<String, LocalDate> lastTradeDateByUserId = new HashMap<>();
Map<String, BigDecimal> lastBuyAmtByUserId = new HashMap<>();
Map<String, Integer> isActiveByUserId = new HashMap<>();
for (LbUser lbUser : lbUsers) {
if (lbUser.getId() == null) {
continue;
}
LbDepartmentUser existing = userRecordMap.get(uid);
if (existing == null || isNewerDepartmentUser(row, existing)) {
userRecordMap.put(uid, row);
String userId = String.valueOf(lbUser.getId());
LocalDate userJoinDate = lbUser.getJoinTime() != null
? lbUser.getJoinTime().toLocalDate() : null;
LbBuyerTradeStats tradeStats = tradeStatsByUserId.get(userId);
LocalDate userLastTradeDate = tradeStats == null
? null : parseOrderTimeToDate(tradeStats.getPayTime());
BigDecimal userLastBuyAmt = tradeStats == null ? null : tradeStats.getTotalMoney();
int userIsActive = tradeStats == null ? 0 : 1;
joinDateByUserId.put(userId, userJoinDate);
lastTradeDateByUserId.put(userId, userLastTradeDate);
lastBuyAmtByUserId.put(userId, userLastBuyAmt);
isActiveByUserId.put(userId, userIsActive);
if (!hasFilter || matchesTreeFilter(
userIsActive, userJoinDate, userLastTradeDate, isActive, joinDate, lastTradeDate)) {
matchedUserIds.add(userId);
}
}
Map<String, String> userIdByRecordId = new HashMap<>();
Set<String> allUserIds = new HashSet<>(userRecordMap.keySet());
for (LbDepartmentUser row : deptUsers) {
String rowId = trimToNull(row.getId());
String uid = trimToNull(row.getUserId());
if (rowId != null && uid != null) {
userIdByRecordId.put(rowId, uid);
}
}
Map<String, String> parentByUserId = new HashMap<>();
for (LbDepartmentUser row : userRecordMap.values()) {
String uid = row.getUserId().trim();
String parentUserId = resolveParentUserId(row, allUserIds, userIdByRecordId);
if (parentUserId != null) {
parentByUserId.put(uid, parentUserId);
}
}
Set<String> visibleUserIds = hasFilter
? includeAncestorUserIds(matchedUserIds, userById)
: new HashSet<>(userById.keySet());
Map<String, Map<String, Object>> nodeByUserId = new LinkedHashMap<>();
for (LbDepartmentUser row : userRecordMap.values()) {
nodeByUserId.put(row.getUserId().trim(), toDepartmentUserTreeNode(row));
Map<String, String> parentByUserId = new HashMap<>();
for (String userId : visibleUserIds) {
LbUser lbUser = userById.get(userId);
if (lbUser == null) {
continue;
}
nodeByUserId.put(userId, toLbUserTreeNode(
lbUser,
joinDateByUserId.get(userId),
lastTradeDateByUserId.get(userId),
lastBuyAmtByUserId.get(userId),
isActiveByUserId.getOrDefault(userId, 0)));
String parentUserId = resolveLbUserParentId(lbUser, userById);
if (parentUserId != null && visibleUserIds.contains(parentUserId)) {
parentByUserId.put(userId, parentUserId);
}
}
List<Map<String, Object>> treeRoots = new ArrayList<>();
@@ -747,11 +746,12 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
children.add(node);
}
sortDepartmentUserTreeByName(treeRoots);
List<Map<String, Object>> treeData = skipFirstTreeLevel(treeRoots);
result.put("success", true);
result.put("message", "查询成功");
result.put("data", treeRoots);
result.put("total", nodeByUserId.size());
result.put("data", treeData);
result.put("total", countTreeNodes(treeData));
result.put("tenantId", tenantIdTrim);
return result;
} catch (Exception e) {
@@ -762,46 +762,122 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
}
/**
* parent_id 兼容两种存法:父 user_id或父记录 id(UUID)
* 去掉第 1 级根节点,将其 children 提升为返回给前端的顶层
*/
private static String resolveParentUserId(LbDepartmentUser row,
Set<String> allUserIds,
Map<String, String> userIdByRecordId) {
String parentRef = trimToNull(row.getParentId());
if (parentRef == null) {
return null;
private static List<Map<String, Object>> skipFirstTreeLevel(List<Map<String, Object>> treeRoots) {
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> root : treeRoots) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) root.get("children");
if (children != null && !children.isEmpty()) {
result.addAll(children);
}
}
if (allUserIds.contains(parentRef)) {
return parentRef;
}
return userIdByRecordId.get(parentRef);
sortDepartmentUserTreeByName(result);
return result;
}
private static Map<String, Object> toDepartmentUserTreeNode(LbDepartmentUser row) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("userId", trimToNull(row.getUserId()));
node.put("name", trimToNull(row.getName()));
node.put("phone", trimToNull(row.getPhone()));
node.put("joinDate", row.getJoinDate());
node.put("lastTradeDate", row.getLastTradeDate());
node.put("lastBuyAmt", row.getLastBuyAmt());
node.put("isActive", row.getIsActive());
node.put("children", new ArrayList<Map<String, Object>>());
return node;
private static int countTreeNodes(List<Map<String, Object>> nodes) {
int count = 0;
for (Map<String, Object> node : nodes) {
count++;
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
if (children != null && !children.isEmpty()) {
count += countTreeNodes(children);
}
}
return count;
}
private static boolean isNewerDepartmentUser(LbDepartmentUser candidate, LbDepartmentUser existing) {
LocalDateTime candidateTime = candidate.getUpdateTime() != null
? candidate.getUpdateTime() : candidate.getCreateTime();
LocalDateTime existingTime = existing.getUpdateTime() != null
? existing.getUpdateTime() : existing.getCreateTime();
if (candidateTime == null) {
private static boolean matchesTreeFilter(int userIsActive,
LocalDate userJoinDate,
LocalDate userLastTradeDate,
Integer isActive,
LocalDate joinDate,
LocalDate lastTradeDate) {
if (isActive != null && userIsActive != isActive) {
return false;
}
if (existingTime == null) {
return true;
if (joinDate != null && (userJoinDate == null || !userJoinDate.isAfter(joinDate))) {
return false;
}
return candidateTime.isAfter(existingTime);
return lastTradeDate == null
|| (userLastTradeDate != null && userLastTradeDate.isAfter(lastTradeDate));
}
/**
* 筛选命中用户时,沿 lb_user.pid 向上补全祖先,避免子节点因上级被筛掉而挂错层级。
*/
private static Set<String> includeAncestorUserIds(Set<String> userIds, Map<String, LbUser> userById) {
Set<String> expanded = new HashSet<>(userIds);
for (String userId : userIds) {
LbUser current = userById.get(userId);
Set<Long> visited = new HashSet<>();
while (current != null) {
Long pid = current.getPid();
if (pid == null || pid <= 0 || !visited.add(pid)) {
break;
}
String parentUserId = String.valueOf(pid);
if (!userById.containsKey(parentUserId)) {
break;
}
expanded.add(parentUserId);
current = userById.get(parentUserId);
}
}
return expanded;
}
/**
* 从 lb_user.pid 解析上级 userIdpid 必须在同租户用户索引中存在。
*/
private static String resolveLbUserParentId(LbUser user, Map<String, LbUser> userById) {
if (user == null || user.getPid() == null || user.getPid() <= 0) {
return null;
}
String parentUserId = String.valueOf(user.getPid());
return userById.containsKey(parentUserId) ? parentUserId : null;
}
/**
* 一次 SQL 从 lb_order_row 汇总每位买家的最新已支付订单统计。
*/
private Map<String, LbBuyerTradeStats> loadBuyerTradeStatsFromOrderRow(String tenantId) {
List<LbBuyerTradeStats> rows = lbOrderRowMapper.selectLatestPaidStatsByTenantId(
tenantId, ORDER_DATA_TYPE_DETAIL);
Map<String, LbBuyerTradeStats> statsByUserId = new HashMap<>();
if (rows == null || rows.isEmpty()) {
return statsByUserId;
}
for (LbBuyerTradeStats row : rows) {
if (row.getBuyerId() == null) {
continue;
}
statsByUserId.put(String.valueOf(row.getBuyerId()), row);
}
return statsByUserId;
}
private static Map<String, Object> toLbUserTreeNode(LbUser user,
LocalDate joinDate,
LocalDate lastTradeDate,
BigDecimal lastBuyAmt,
int isActive) {
Map<String, Object> node = new LinkedHashMap<>();
node.put("userId", user.getId() == null ? null : String.valueOf(user.getId()));
node.put("name", trimToNull(resolveDisplayName(user)));
node.put("phone", trimToNull(user.getMobile()));
Long pid = user.getPid();
node.put("parentId", pid == null || pid <= 0 ? null : String.valueOf(pid));
node.put("parentName", trimToNull(user.getPname()));
node.put("joinDate", joinDate);
node.put("lastTradeDate", lastTradeDate);
node.put("lastBuyAmt", lastBuyAmt);
node.put("isActive", isActive);
node.put("children", new ArrayList<Map<String, Object>>());
return node;
}
private static void sortDepartmentUserTreeByName(List<Map<String, Object>> nodes) {

View File

@@ -59,4 +59,29 @@
today_order_count = VALUES(today_order_count)
</insert>
<select id="selectLatestPaidStatsByTenantId" resultType="com.rj.dto.LbBuyerTradeStats">
SELECT o.buyer_id AS buyerId,
o.pay_time AS payTime,
o.total_money AS totalMoney
FROM lb_order_row o
WHERE o.tenant_id = #{tenantId}
AND o.data_type = #{dataType}
AND o.buyer_id IS NOT NULL
AND o.pay_time IS NOT NULL
AND TRIM(o.pay_time) != ''
AND NOT EXISTS (
SELECT 1
FROM lb_order_row o2
WHERE o2.tenant_id = o.tenant_id
AND o2.data_type = o.data_type
AND o2.buyer_id = o.buyer_id
AND o2.pay_time IS NOT NULL
AND TRIM(o2.pay_time) != ''
AND (
o2.pay_time &gt; o.pay_time
OR (o2.pay_time = o.pay_time AND o2.id &gt; o.id)
)
)
</select>
</mapper>