构造属性结构

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.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rj.dto.LbBuyerTradeStats;
import com.rj.entity.LbOrderRow; import com.rj.entity.LbOrderRow;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -16,4 +17,10 @@ public interface LbOrderRowMapper extends BaseMapper<LbOrderRow> {
*/ */
@InterceptorIgnore(tenantLine = "true") @InterceptorIgnore(tenantLine = "true")
int upsertBatch(@Param("list") List<LbOrderRow> list); 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); 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); 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.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rj.dto.LbBuyerTradeStats;
import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTrade;
import com.rj.entity.LbDepartmentUser; import com.rj.entity.LbDepartmentUser;
import com.rj.entity.LbOrderRow; import com.rj.entity.LbOrderRow;
@@ -492,27 +493,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
} }
} }
Set<String> tradeActiveUserIds = new HashSet<>(); Map<String, LbBuyerTradeStats> tradeStatsByUserId = loadBuyerTradeStatsFromOrderRow(tenantIdTrim);
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);
}
}
}
List<LbDepartmentUser> toInsert = new ArrayList<>(); List<LbDepartmentUser> toInsert = new ArrayList<>();
List<LbDepartmentUser> toUpdate = new ArrayList<>(); List<LbDepartmentUser> toUpdate = new ArrayList<>();
@@ -556,16 +537,17 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
entity.setOrgChain(buildOrgChain(lbUser.getId(), userById)); entity.setOrgChain(buildOrgChain(lbUser.getId(), userById));
LbOrderRow latestOrder = latestOrderByBuyerId.get(userId); LbBuyerTradeStats tradeStats = tradeStatsByUserId.get(userId);
if (latestOrder != null) { if (tradeStats != null) {
entity.setLastTradeDate(parseOrderTimeToDate(latestOrder.getPayTime())); entity.setLastTradeDate(parseOrderTimeToDate(tradeStats.getPayTime()));
entity.setLastBuyAmt(latestOrder.getTotalMoney()); entity.setLastBuyAmt(tradeStats.getTotalMoney());
entity.setIsActive(1);
} else { } else {
entity.setLastTradeDate(null); entity.setLastTradeDate(null);
entity.setLastBuyAmt(null); entity.setLastBuyAmt(null);
entity.setIsActive(0);
} }
entity.setUpdateTime(now); entity.setUpdateTime(now);
entity.setIsActive(tradeActiveUserIds.contains(userId) ? 1 : 0);
if (isInsert) { if (isInsert) {
toInsert.add(entity); toInsert.add(entity);
@@ -654,7 +636,7 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
.replace(ORG_CHAIN_NODE_SEPARATOR, ""); .replace(ORG_CHAIN_NODE_SEPARATOR, "");
} }
private String resolveDisplayName(LbUser user) { private static String resolveDisplayName(LbUser user) {
if (user.getNickname() != null && !user.getNickname().trim().isEmpty()) { if (user.getNickname() != null && !user.getNickname().trim().isEmpty()) {
return user.getNickname().trim(); return user.getNickname().trim();
} }
@@ -675,19 +657,9 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
} }
String tenantIdTrim = tenantId.trim(); String tenantIdTrim = tenantId.trim();
LambdaQueryWrapper<LbDepartmentUser> queryWrapper = new LambdaQueryWrapper<LbDepartmentUser>() List<LbUser> lbUsers = lbUserMapper.selectList(
.eq(LbDepartmentUser::getTenantId, tenantIdTrim); new LambdaQueryWrapper<LbUser>().eq(LbUser::getTenantId, tenantIdTrim));
if (isActive != null) { if (lbUsers == null || lbUsers.isEmpty()) {
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()) {
result.put("success", true); result.put("success", true);
result.put("message", "查询成功"); result.put("message", "查询成功");
result.put("data", new ArrayList<>()); result.put("data", new ArrayList<>());
@@ -696,40 +668,67 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
return result; return result;
} }
Map<String, LbDepartmentUser> userRecordMap = new LinkedHashMap<>(); Map<String, LbUser> userById = new LinkedHashMap<>();
for (LbDepartmentUser row : deptUsers) { for (LbUser lbUser : lbUsers) {
String uid = trimToNull(row.getUserId()); if (lbUser.getId() != null) {
if (uid == 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; continue;
} }
LbDepartmentUser existing = userRecordMap.get(uid); String userId = String.valueOf(lbUser.getId());
if (existing == null || isNewerDepartmentUser(row, existing)) { LocalDate userJoinDate = lbUser.getJoinTime() != null
userRecordMap.put(uid, row); ? 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> visibleUserIds = hasFilter
Set<String> allUserIds = new HashSet<>(userRecordMap.keySet()); ? includeAncestorUserIds(matchedUserIds, userById)
for (LbDepartmentUser row : deptUsers) { : new HashSet<>(userById.keySet());
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);
}
}
Map<String, Map<String, Object>> nodeByUserId = new LinkedHashMap<>(); Map<String, Map<String, Object>> nodeByUserId = new LinkedHashMap<>();
for (LbDepartmentUser row : userRecordMap.values()) { Map<String, String> parentByUserId = new HashMap<>();
nodeByUserId.put(row.getUserId().trim(), toDepartmentUserTreeNode(row)); 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<>(); List<Map<String, Object>> treeRoots = new ArrayList<>();
@@ -747,11 +746,12 @@ public class LbDepartmentUserServiceImpl extends ServiceImpl<LbDepartmentUserMap
children.add(node); children.add(node);
} }
sortDepartmentUserTreeByName(treeRoots); sortDepartmentUserTreeByName(treeRoots);
List<Map<String, Object>> treeData = skipFirstTreeLevel(treeRoots);
result.put("success", true); result.put("success", true);
result.put("message", "查询成功"); result.put("message", "查询成功");
result.put("data", treeRoots); result.put("data", treeData);
result.put("total", nodeByUserId.size()); result.put("total", countTreeNodes(treeData));
result.put("tenantId", tenantIdTrim); result.put("tenantId", tenantIdTrim);
return result; return result;
} catch (Exception e) { } 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, private static List<Map<String, Object>> skipFirstTreeLevel(List<Map<String, Object>> treeRoots) {
Set<String> allUserIds, List<Map<String, Object>> result = new ArrayList<>();
Map<String, String> userIdByRecordId) { for (Map<String, Object> root : treeRoots) {
String parentRef = trimToNull(row.getParentId()); @SuppressWarnings("unchecked")
if (parentRef == null) { List<Map<String, Object>> children = (List<Map<String, Object>>) root.get("children");
return null; 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) { private static int countTreeNodes(List<Map<String, Object>> nodes) {
Map<String, Object> node = new LinkedHashMap<>(); int count = 0;
node.put("userId", trimToNull(row.getUserId())); for (Map<String, Object> node : nodes) {
node.put("name", trimToNull(row.getName())); count++;
node.put("phone", trimToNull(row.getPhone())); @SuppressWarnings("unchecked")
node.put("joinDate", row.getJoinDate()); List<Map<String, Object>> children = (List<Map<String, Object>>) node.get("children");
node.put("lastTradeDate", row.getLastTradeDate()); if (children != null && !children.isEmpty()) {
node.put("lastBuyAmt", row.getLastBuyAmt()); count += countTreeNodes(children);
node.put("isActive", row.getIsActive()); }
node.put("children", new ArrayList<Map<String, Object>>()); }
return node; return count;
} }
private static boolean isNewerDepartmentUser(LbDepartmentUser candidate, LbDepartmentUser existing) { private static boolean matchesTreeFilter(int userIsActive,
LocalDateTime candidateTime = candidate.getUpdateTime() != null LocalDate userJoinDate,
? candidate.getUpdateTime() : candidate.getCreateTime(); LocalDate userLastTradeDate,
LocalDateTime existingTime = existing.getUpdateTime() != null Integer isActive,
? existing.getUpdateTime() : existing.getCreateTime(); LocalDate joinDate,
if (candidateTime == null) { LocalDate lastTradeDate) {
if (isActive != null && userIsActive != isActive) {
return false; return false;
} }
if (existingTime == null) { if (joinDate != null && (userJoinDate == null || !userJoinDate.isAfter(joinDate))) {
return true; 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) { private static void sortDepartmentUserTreeByName(List<Map<String, Object>> nodes) {

View File

@@ -59,4 +59,29 @@
today_order_count = VALUES(today_order_count) today_order_count = VALUES(today_order_count)
</insert> </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> </mapper>