如何使用 Map 获取唯一的 User Agent Name
结论
不要用 User.Name 去重,也不要反复把同一个全局 oppList 放进 oppMap。Map 应以唯一的 OwnerId 为 Key,并为每个 Owner 创建独立的汇总对象,分别累加付款数量。
付款查询也需要移到 Opportunity 循环之外。原代码不仅会产生 N+1 SOQL,payList 和 payList1 也没有按 Opportunity 或 Owner 清空,付款数量因此会不断累积。
推荐实现
下面的代码先查询 Opportunity 与 Owner 的对应关系,再通过聚合查询统计每个 Opportunity 的付款数量,最后按 OwnerId 汇总。返回结果中,每个用户只会出现一次。
public with sharing class First_Pay_ClearRates_Controller {
public class AgentPaymentSummary {
@AuraEnabled public Id ownerId { get; set; }
@AuraEnabled public String ownerName { get; set; }
@AuraEnabled public Integer totalPaymentCount { get; set; }
@AuraEnabled public Integer collectedPaymentCount { get; set; }
@AuraEnabled public Integer pendingPaymentCount { get; set; }
public AgentPaymentSummary(Id ownerId, String ownerName) {
this.ownerId = ownerId;
this.ownerName = ownerName;
this.totalPaymentCount = 0;
this.collectedPaymentCount = 0;
this.pendingPaymentCount = 0;
}
}
@AuraEnabled(cacheable=true)
public static Map<Id, User> fetchUsers() {
return new Map<Id, User>([
SELECT Id, Name
FROM User
WHERE Profile.Name = 'Sales'
LIMIT 20
]);
}
@AuraEnabled(cacheable=true)
public static List<AgentPaymentSummary> getPaymentSummary(
List<Id> userIds
) {
if (userIds == null || userIds.isEmpty()) {
return new List<AgentPaymentSummary>();
}
Map<Id, AgentPaymentSummary> summaryByOwner =
new Map<Id, AgentPaymentSummary>();
Map<Id, Id> ownerByOpportunity = new Map<Id, Id>();
for (Opportunity opp : [
SELECT Id, OwnerId, Owner.Name
FROM Opportunity
WHERE OwnerId IN :userIds
AND First_Payment_Date__c != null
]) {
ownerByOpportunity.put(opp.Id, opp.OwnerId);
if (!summaryByOwner.containsKey(opp.OwnerId)) {
summaryByOwner.put(
opp.OwnerId,
new AgentPaymentSummary(opp.OwnerId, opp.Owner.Name)
);
}
}
if (ownerByOpportunity.isEmpty()) {
return new List<AgentPaymentSummary>();
}
for (AggregateResult result : [
SELECT Opportunity__c,
Payment_Collected__c,
COUNT(Id)
FROM Payments__c
WHERE Opportunity__c IN :ownerByOpportunity.keySet()
GROUP BY Opportunity__c, Payment_Collected__c
]) {
Id opportunityId = (Id) result.get('Opportunity__c');
Boolean collected =
(Boolean) result.get('Payment_Collected__c');
Integer paymentCount =
(Integer) result.get('expr0');
Id ownerId = ownerByOpportunity.get(opportunityId);
AgentPaymentSummary summary = summaryByOwner.get(ownerId);
summary.totalPaymentCount += paymentCount;
if (collected == true) {
summary.collectedPaymentCount += paymentCount;
} else {
summary.pendingPaymentCount += paymentCount;
}
}
return summaryByOwner.values();
}
}
前端调用 getPaymentSummary 时传入用户 ID:
const userIds = Object.keys(userMap);
getPaymentSummary({ userIds: userIds })
.then(result => {
this.agentSummaries = result;
});
为什么原代码会出现重复和错误计数
Name 不是可靠的唯一标识
Salesforce 允许不同用户使用相同的姓名,因此不能用 Name 作为唯一标识。应使用 User.Id,对应到 Opportunity 上就是 OwnerId。
Map<Id, AgentPaymentSummary> summaryByOwner;
页面仍然可以显示 Owner.Name,但去重和汇总必须以 OwnerId 为准。
所有 Owner 共用了同一个 oppList
原代码执行了以下操作:
oppList.add(o);
oppMap.put(o.OwnerId, oppList);
每个 Key 对应的都是同一个 oppList 实例。这意味着不同 Owner 在 Map 中实际共享同一份 Opportunity 列表。
如果确实需要使用 Map<Id, List<Opportunity>>,可以按 Owner 分别创建列表:
Map<Id, List<Opportunity>> oppsByOwner =
new Map<Id, List<Opportunity>>();
for (Opportunity opp : opportunities) {
if (!oppsByOwner.containsKey(opp.OwnerId)) {
oppsByOwner.put(opp.OwnerId, new List<Opportunity>());
}
oppsByOwner.get(opp.OwnerId).add(opp);
}
不过,如果目标是让一名 Agent 对应一组付款数量,直接返回汇总 DTO 更合适,没有必要返回重复的 Opportunity。
付款列表没有重置
payList 和 payList1 定义在循环外:
List<Payments__c> payList = new List<Payments__c>();
List<Payments__c> payList1 = new List<Payments__c>();
处理下一条 Opportunity 时,上一条 Opportunity 的付款记录仍保留在列表中。因此,size() 返回的是此前所有已处理记录的累计数量,而不是当前 Opportunity 或当前 Owner 的付款数量。
Opportunity 循环内查询 Payments 会触发查询限制
下面的写法会为每条 Opportunity 单独执行一次 SOQL:
for (Opportunity opp : opportunities) {
for (Payments__c payment : [
SELECT Id
FROM Payments__c
WHERE Opportunity__c = :opp.Id
]) {
// ...
}
}
当 Opportunity 数量增加时,这种写法很容易达到 Salesforce 单个事务的 SOQL 查询次数限制。推荐实现只需要执行两次主要查询:
- 查询符合条件的 Opportunity。
- 聚合查询相关的
Payments__c。
注意事项
- 示例把
Payment_Collected__c = true计为已收款,把false或null计为待收款。如果业务需要区分false和null,还要增加单独的判断。 - 不要依赖
LIMIT 20完成正式汇总,因为它只统计查询返回的部分记录。如果需要分页,应明确分页范围,并保证统计口径一致。 Profile.Name = 'Sales'依赖当前组织中的 Profile 名称。如果该名称可能调整,应改用更稳定的业务条件筛选用户。- 示例只返回临时汇总数据,不会更新
Opportunity.PaymentsCounter__c或CollectedPaymentCount__c。如果需要把结果写回 Opportunity,还要单独执行 DML,并先明确这些字段是按 Opportunity 还是按 Owner 保存。 Map.values()不保证返回结果的显示顺序。如果页面需要按 Agent Name 排序,可以在 Apex 中对 DTO 排序,也可以由前端对返回数组排序。
备注:内容仅供参考。
