PAYATHON 2026

如何使用 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 查询次数限制。推荐实现只需要执行两次主要查询:

  1. 查询符合条件的 Opportunity。
  2. 聚合查询相关的 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 排序,也可以由前端对返回数组排序。

备注:内容仅供参考。