大型经理帐号层级结构模板

AdWords 经理帐号可以管理数百甚至数千个客户帐号。对所有这些帐号运行脚本可能颇具挑战性,因为使用 executeInParallel 进行并行处理会将您的脚本限制为每次执行最多 50 个帐号,如果有太多的帐号,对它们依序遍历可能会导致您的脚本超时。

“大型经理帐号层级结构模板”脚本在每次执行时处理帐号的不同子集,从而解决了这一问题。每次执行都会将其中间结果存储在 Google 云端硬盘上的临时文件中,以使后续执行过程知道还有哪些帐号等待处理。经过多次运行,该脚本将最终遍历并处理所有客户帐号。如果处理完了最后剩下的帐号,它将重置并开始新的处理周期(取决于指定的频率)。

模板分为两个部分。使用标为 STANDARD TEMPLATE 的部分提供的功能,可通过多次执行脚本实现对帐号的遍历,不需要您对其进行修改。标为 YOUR IMPLEMENTATION 的部分提供了几个占位符函数(或称“挂钩”),以便您填写希望对帐号实际执行的处理逻辑。下面的源代码示例演示了这一实现方式。

在这一页中,术语执行指的是脚本的单次执行,而术语周期指的是脚本经过多次执行最终处理完所有帐号。

配置脚本

对该模板的使用包括三个步骤:

  1. 用模板创建新的脚本。
  2. 设置模板级参数。
  3. 提供核心逻辑。

用模板创建新的脚本

使用下面的源代码设置新脚本。在 TEMPLATE_CONFIG 对象中,为用来在云端硬盘中保存中间结果的文件提供 FILENAME。该脚本将在第一次执行时创建这个文件,并在后续执行中重复使用。您可以任意选择 FILENAME,但名称最好与脚本名称类似。这将便于您以后识别该文件(如果需要)。

对于您的帐号中使用此模板的所有脚本,该 FILENAME 必须是唯一的。否则,不同脚本的中间数据会彼此覆盖。该 FILENAME 也应该不同于您的云端硬盘帐号中现有的任何其他文件。

设置模板级参数

有几个模板级参数需要设置。

  • MIN_FREQUENCY:控制周期的最小频率。从前一周期启动算起,至少经过这个天数,才会启动下一个周期。请注意,如果一个周期本身需要花费的时间即长于 MIN_FREQUENCY,则实际频率可能会更长。原因可能包括有大量帐号等待处理,或者脚本本身安排的运行频率过低,无法在 MIN_FREQUENCY 天中完成遍历过程。
  • USE_PARALLEL_MODE:控制脚本是并行处理帐号还是依序进行帐号遍历
  • MAX_ACCOUNTS:控制脚本将试图在一次执行中处理的帐号最大数目。当在并行模式下运行时,实际处理的帐号数目上限是 50。
  • ACCOUNT_CONDITIONS(可选):控制脚本将处理哪些帐号的 ManagedAccountSelector 条件数组。例如,您可以添加 'LabelNames CONTAINS "ACCOUNT_LABEL"' 等条件,只处理带有特定标签的帐号。

请参阅下面的运行时间设置,了解有关如何针对不同使用情况设置这些参数的建议。

提供您的核心逻辑

除非您有自己的逻辑,并通过对帐号执行所需的实际处理过程将其实现,否则模板毫无用处。为做到这一点,该模板提供了 5 个可在每个周期中的特定时间调用的函数(也称“挂钩”),如下所示。您可以为每个“挂钩”提供实现方式,以便针对您的脚本执行所需处理过程。

对于每一个“挂钩”,该模板都包括了应该由您自己的逻辑来代替的示例实现方式。您也可以像平常一样,向脚本添加自己的函数和变量,并从这 5 个“挂钩”中使用它们。

initializeCycle(customerIds)
在每个周期的开始会调用这个函数一次,并列出将被处理的帐号(匹配 ACCOUNT_CONDITIONS 的帐号)。用来执行您的脚本在整个周期中需要的任何初始化过程。一些可能的使用案例有:
  • 发送电子邮件,指示周期已开始。
  • 加载或生成希望对周期中的所有脚本执行保持一致的静态数据。必须将该数据保存在云端硬盘或电子表格等位置,以便可以在以后每次执行时加载。
如果没有要执行的初始化,请将这一实现留空。
initializeExecution(customerIds)
在每次执行的开始会调用这个函数一次,并列出将在这次执行中处理的帐号子集。用来执行您的脚本在每次执行中需要的任何初始化过程。一些可能的使用案例有:
  • 发送电子邮件,指示执行已开始。
  • 加载希望在所有执行中保持一致的静态数据(参加上文)。
如果没有要执行的初始化,请将这一实现留空。
processAccount()
在一个周期内,对每个 AdWords 帐号会调用此函数一次。用来执行脚本的主要处理逻辑,如获取和分析报告,暂停/取消暂停广告系列,修改出价,制作广告,发送电子邮件提醒,将数据保存到电子表格或其他任何可以使用 AdWords 脚本完成的工作。或者,这个函数可以返回与处理该帐号关联的任意结果。返回结果之后,您就可以在输出前对所有帐号的结果进行汇总,如发送一份可涵盖所有帐号结果的电子邮件提醒,而不是每个帐号发送一封电子邮件(见下文)。
processIntermediateResults(results)
在每次执行的结束会调用这个函数一次,并列出这次执行中处理的帐号结果。用来分析、汇总和输出这次执行的结果。一些可能的使用案例有:
  • 将这次执行的结果添加到电子表格中。
  • 根据这次执行的所有结果,创建和发送一封摘要电子邮件。
如果您没有任何结果要输出,或者如果只想在整个周期完成后输出,请将这一实现留空。
processFinalResults(results)
在每个周期结束时会调用这个函数一次,并列出这一周期中处理的所有帐号的结果。用来分析、汇总和输出该周期中包含的所有帐号的结果。一些可能的使用案例有:
  • 执行要求所有帐号都有结果的分析。
  • 将所有结果同时添加到电子表格。
  • 根据所有帐号的所有结果,创建和发送一封摘要电子邮件。
如果没有要输出的任何结果,请将这一实现留空。

运行时间设置

相比其他脚本,安排使用经理帐号模板的脚本的运行时间,需要稍微多想一下,因为您必须考虑整个周期,而不是单次执行。大多数用户都希望每个周期尽快完成。具体操作步骤如下:

  • 安排 AdWords 中的脚本每小时运行一次,这是最高的频率设置。
  • 估计 processAccount() 对单个帐号需要执行多长时间。生成估计结果的一种方式是设置 ACCOUNT_CONDITIONS,使其只对几个帐号进行操作,并依序运行该脚本,然后用执行时间除以帐号数。
  • 设置 USE_PARALLEL_MODEMAX_ACCOUNTS 如下。
    • 依序运行的脚本可在超时前运行 30 分钟(1,800 秒)。在并行模式下运行的脚本可以对多达 50 个帐号进行操作。哪种模式可在每次执行中处理更多帐号,取决于您的脚本处理每一个帐号需要多长时间。
    • 如果您估计每个帐号的处理时间少于 36 秒 (1,800 / 50),则最好使用依序模式。同样,如果您估计每个帐号的处理时间超过 36 秒,则最好使用并行模式。考虑到每个帐号的处理时间可能有变化以及额外的后处理过程所需的一些补充时间,合理的分界点可能是 30,而不是 36。
    • 如果使用的是并行模式,请将 MAX_ACCOUNTS 设置为 50。指定更大的数字没有意义,因为上限就是 50。
    • 如果使用的是依序模式,请尽可能将 MAX_ACCOUNTS 设置为可在 30 分钟内处理的最大数字。例如,如果您估计每个帐号可以在 10 秒内处理完毕,则可以将 MAX_ACCOUNTS 设置为 180。在实际使用时,将数字设置得略小一些,可能更有助于为您的 processIntermediateResults()processFinalResults() 实现留出运行时间。

尽管已经安排每小时在 AdWords 中运行一次,但仍可以设置 MIN_FREQUENCY 来间隔开各个周期。例如,如果希望每周生成一次涵盖所有帐号的电子表格报告,请在 processFinalResults() 挂钩中生成电子表格,并将 MIN_FREQUENCY 设置为 7。

工作原理

模板的核心位于标有 STANDARD TEMPLATE 的部分中,其中包含脚本的 main() 函数。使用 stateManager 对象所公开的方法,可加载和保存周期在以前执行中的状态。

在每次执行的开始,main() 会加载之前的状态,如果自上次完成周期已经过了足够长的时间,会启动新的周期,会确定要处理的帐号子集,需要的话会调用 initializeCycle() 挂钩,并调用 initializeExecution() 挂钩。然后,它会调用 executeByMode(),利用依序或并行策略处理帐号的子集。这两种策略会为子集中的每个帐号调用 processAccount() 挂钩,并将最终的结果传递给 completeExecution(),以便将其保存到云端硬盘。最后,该模板会调用 processIntermediateResults() 挂钩,并在该周期结束时调用 processFinalResults 挂钩。

尽管该模板适合应用于经理帐号,不过它也能成功地运行于单一帐号。如果要开发和调试核心逻辑,则在单一帐号中运行该模板会很有帮助。也可以将以前在经理帐号中使用的脚本应用于单一帐号,而且除了 FILENAME 之外,无需进行任何修改。

设置

  • 使用下面的源代码设置脚本。
  • 按照配置脚本下的步骤完成设置。

源代码

// Copyright 2016, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name Large Manager Hierarchy Template
 *
 * @overview The Large Manager Hierarchy Template script provides a general way
 *     to run script logic on all client accounts within a manager account
 *     hierarchy, splitting the work across multiple executions if necessary.
 *     Each execution of the script processes a subset of the hierarchy's client
 *     accounts that it hadn't previously processed, saving the results to a
 *     temporary file on Drive. Once the script processes the final subset of
 *     accounts, the consolidated results can be output and the cycle can begin
 *     again.
 *     See
 *     https://developers.google.com/adwords/scripts/docs/solutions/mccapp-manager-template
 *     for more details.
 *
 * @author AdWords Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 1.0
 *
 * @changelog
 * - version 1.0
 *   - Released initial version.
 */

/*************** START OF YOUR IMPLEMENTATION ***************/

var TEMPLATE_CONFIG = {
  // The name of the file that will be created on Drive to store data
  // between executions of the script. You must use a different
  // filename for each each script running in the account, or data
  // from different scripts may overwrite one another.
  FILENAME: 'UNIQUE_FILENAME_HERE',

  // The minimum number of days between the start of each cycle.
  MIN_FREQUENCY: 1,

  // Controls whether child accounts will be processed in parallel (true)
  // or sequentially (false).
  USE_PARALLEL_MODE: true,

  // Controls the maximum number of accounts that will be processed in a
  // single script execution.
  MAX_ACCOUNTS: 50,

  // A list of ManagedAccountSelector conditions to restrict the population
  // of child accounts that will be processed. Leave blank or comment out
  // to include all child accounts.
  ACCOUNT_CONDITIONS: []
};

// The possible statuses for the script as a whole or an individual account.
var Statuses = {
  NOT_STARTED: 'Not Started',
  STARTED: 'Started',
  FAILED: 'Failed',
  COMPLETE: 'Complete'
};

/**
 * Your main logic for initializing a cycle for your script.
 *
 * @param {Array.<string>} customerIds The customerIds that this cycle
 *     will process.
 */
function initializeCycle(customerIds) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this cycle.
  Logger.log('Accounts to be processed this cycle:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * Your main logic for initializing a single execution of the script.
 *
 * @param {Array.<string>} customerIds The customerIds that this
 *     execution will process.
 */
function initializeExecution(customerIds) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply prints the accounts that will be process in
  // this execution.
  Logger.log('Accounts to be processed this execution:');
  for (var i = 0; i < customerIds.length; i++) {
    Logger.log(customerIds[i]);
  }
}

/**
 * Your main logic for processing a single AdWords account. This function
 * can perform any sort of processing on the account, followed by
 * outputting results immediately (e.g., sending an email, saving to a
 * spreadsheet, etc.) and/or returning results to be output later, e.g.,
 * to be combined with the output from other accounts.
 *
 * @return {Object} An object containing any results of your processing
 *    that you want to output later.
 */
function processAccount() {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply returns the number of campaigns and ad groups
  // in the account.
  return {
    numCampaigns: AdWordsApp.campaigns().get().totalNumEntities(),
    numAdGroups: AdWordsApp.adGroups().get().totalNumEntities()
  };
}

/**
 * Your main logic for consolidating or outputting results after
 * a single execution of the script. These single execution results may
 * reflect the processing on only a subset of your accounts.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    Statuses.COMPLETE if the account was processed successfully,
 *    Statuses.FAILED if there was an error, and Statuses.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    Statuses.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    Statuses.FAILED.
 */
function processIntermediateResults(results) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This example simply logs the number of campaigns and ad groups
  // in each of the accounts successfully processed in this execution.
  Logger.log('Results of this execution:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      Logger.log(customerId + ': ' + result.returnValue.numCampaigns +
                 ' campaigns, ' + result.returnValue.numAdGroups +
                 ' ad groups');
    } else if (result.status == Statuses.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }
}

/**
 * Your main logic for consolidating or outputting results after
 * the script has executed a complete cycle across all of your accounts.
 * This function will only be called once per complete cycle.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results for the accounts processed in this
 *    execution of the script, keyed by customerId. The status will be
 *    Statuses.COMPLETE if the account was processed successfully,
 *    Statuses.FAILED if there was an error, and Statuses.STARTED if it
 *    timed out. The returnValue field is present when the status is
 *    Statuses.COMPLETE and corresponds to the object you returned in
 *    processAccount(). The error field is present when the status is
 *    Statuses.FAILED.
 */
function processFinalResults(results) {
  // REPLACE WITH YOUR IMPLEMENTATION

  // This template simply logs the total number of campaigns and ad
  // groups across all accounts successfully processed in the cycle.
  var numCampaigns = 0;
  var numAdGroups = 0;

  Logger.log('Results of this cycle:');
  for (var customerId in results) {
    var result = results[customerId];
    if (result.status == Statuses.COMPLETE) {
      Logger.log(customerId + ': successful');
      numCampaigns += result.returnValue.numCampaigns;
      numAdGroups += result.returnValue.numAdGroups;
    } else if (result.status == Statuses.STARTED) {
      Logger.log(customerId + ': timed out');
    } else {
      Logger.log(customerId + ': failed due to "' + result.error + '"');
    }
  }

  Logger.log('Total number of campaigns: ' + numCampaigns);
  Logger.log('Total number of ad groups: ' + numAdGroups);
}

/**************** END OF YOUR IMPLEMENTATION ****************/

/**************** START OF STANDARD TEMPLATE ****************/

// Whether or not the script is running in a manager account.
var IS_MANAGER = typeof MccApp !== 'undefined';

// The maximum number of accounts that can be processed when using
// executeInParallel().
var MAX_PARALLEL = 50;

// The possible modes in which the script can execute.
var Modes = {
  SINGLE: 'Single',
  MANAGER_SEQUENTIAL: 'Manager Sequential',
  MANAGER_PARALLEL: 'Manager Parallel'
};

function main() {
  var mode = getMode();
  stateManager.loadState();

  // The last execution may have attempted the final set of accounts but
  // failed to actually complete the cycle because of a timeout in
  // processIntermediateResults(). In that case, complete the cycle now.
  if (stateManager.getAccountsWithStatus().length > 0) {
    completeCycleIfNecessary();
  }

  // If the cycle is complete and enough time has passed since the start of
  // the last cycle, reset it to begin a new cycle.
  if (stateManager.getStatus() == Statuses.COMPLETE) {
    if (dayDifference(stateManager.getLastStartTime(), new Date()) >
        TEMPLATE_CONFIG.MIN_FREQUENCY) {
      stateManager.resetState();
    } else {
      Logger.log('Waiting until ' + TEMPLATE_CONFIG.MIN_FREQUENCY +
                 ' days have elapsed since the start of the last cycle.');
      return;
    }
  }

  // Find accounts that have not yet been processed. If this is the
  // beginning of a new cycle, this will be all accounts.
  var customerIds =
      stateManager.getAccountsWithStatus(Statuses.NOT_STARTED);

  // The status will be Statuses.NOT_STARTED if this is the very first
  // execution or if the cycle was just reset. In either case, it is the
  // beginning of a new cycle.
  if (stateManager.getStatus() == Statuses.NOT_STARTED) {
    stateManager.setStatus(Statuses.STARTED);
    stateManager.saveState();

    initializeCycle(customerIds);
  }

  // Don't attempt to process more accounts than specified, and
  // enforce the limit on parallel execution if necessary.
  var accountLimit = TEMPLATE_CONFIG.MAX_ACCOUNTS;

  if (mode == Modes.MANAGER_PARALLEL) {
    accountLimit = Math.min(MAX_PARALLEL, accountLimit);
  }

  var customerIdsToProcess = customerIds.slice(0, accountLimit);

  // Save state so that we can detect when an account timed out by it still
  // being in the STARTED state.
  stateManager.setAccountsWithStatus(customerIdsToProcess, Statuses.STARTED);
  stateManager.saveState();

  initializeExecution(customerIdsToProcess);
  executeByMode(mode, customerIdsToProcess);
}

/**
 * Runs the script on a list of accounts in a given mode.
 *
 * @param {string} mode The mode the script should run in.
 * @param {Array.<string>} customerIds The customerIds that this execution
 *     should process. If mode is Modes.SINGLE, customerIds must contain
 *     a single element which is the customerId of the AdWords account.
 */
function executeByMode(mode, customerIds) {
  switch (mode) {
    case Modes.SINGLE:
      var results = {};
      results[customerIds[0]] = tryProcessAccount();
      completeExecution(results);
      break;

    case Modes.MANAGER_SEQUENTIAL:
      var accounts = MccApp.accounts().withIds(customerIds).get();
      var results = {};

      var managerAccount = AdWordsApp.currentAccount();
      while (accounts.hasNext()) {
        var account = accounts.next();
        MccApp.select(account);
        results[account.getCustomerId()] = tryProcessAccount();
      }
      MccApp.select(managerAccount);

      completeExecution(results);
      break;

    case Modes.MANAGER_PARALLEL:
      if (customerIds.length == 0) {
        completeExecution({});
      } else {
        var accountSelector = MccApp.accounts().withIds(customerIds);
        accountSelector.executeInParallel('parallelFunction',
                                          'parallelCallback');
      }
      break;
  }
}

/**
 * Attempts to process the current AdWords account.
 *
 * @return {Object} The result of the processing if successful, or
 *     an object with status Statuses.FAILED and the error message
 *     if unsuccessful.
 */
function tryProcessAccount() {
  try {
    return {
      status: Statuses.COMPLETE,
      returnValue: processAccount()
    };
  } catch (e) {
    return {
      status: Statuses.FAILED,
      error: e.message
    };
  }
}

/**
 * The function given to executeInParallel() when running in parallel mode.
 * This helper function is necessary so that the return value of
 * processAccount() is transformed into a string as required by
 * executeInParallel().
 *
 * @return {string} JSON string representing the return value of
 *     processAccount().
 */
function parallelFunction() {
  var returnValue = processAccount();
  return JSON.stringify(returnValue);
}

/**
 * The callback given to executeInParallel() when running in parallel mode.
 * Processes the execution results into the format used by all execution
 * modes.
 *
 * @param {Array.<Object>} executionResults An array of execution results
 *     from a parallel execution.
 */
function parallelCallback(executionResults) {
  var results = {};

  for (var i = 0; i < executionResults.length; i++) {
    var executionResult = executionResults[i];
    var status;

    if (executionResult.getStatus() == 'OK') {
      status = Statuses.COMPLETE;
    } else if (executionResult.getStatus() == 'TIMEOUT') {
      status = Statuses.STARTED;
    } else {
      status = Statuses.FAILED;
    }

    results[executionResult.getCustomerId()] = {
      status: status,
      returnValue: JSON.parse(executionResult.getReturnValue()),
      error: executionResult.getError()
    };
  }

  // After executeInParallel(), variables in global scope are reevaluated,
  // so reload the state.
  stateManager.loadState();

  completeExecution(results);
}

/**
 * Completes a single execution of the script by saving the results and
 * calling the intermediate and final result handlers as necessary.
 *
 * @param {Object.<string, {
 *       status: string,
 *       returnValue: Object,
 *       error: string
 *     }>} results The results of the current execution of the script.
 */
function completeExecution(results) {
  for (var customerId in results) {
    var result = results[customerId];
    stateManager.setAccountWithResult(customerId, result);
  }
  stateManager.saveState();

  processIntermediateResults(results);
  completeCycleIfNecessary();
}

/**
 * Completes a full cycle of the script if all accounts have been attempted
 * but the cycle has not been marked as complete yet.
 */
function completeCycleIfNecessary() {
  if (stateManager.getAccountsWithStatus(Statuses.NOT_STARTED).length == 0 &&
      stateManager.getStatus() != Statuses.COMPLETE) {
    stateManager.setStatus(Statuses.COMPLETE);
    stateManager.saveState();
    processFinalResults(stateManager.getResults());
  }
}

/**
 * Determines what mode the script should run in.
 *
 * @return {string} The mode to run in.
 */
function getMode() {
  if (IS_MANAGER) {
    if (TEMPLATE_CONFIG.USE_PARALLEL_MODE) {
      return Modes.MANAGER_PARALLEL;
    } else {
      return Modes.MANAGER_SEQUENTIAL;
    }
  } else {
    return Modes.SINGLE;
  }
}

/**
 * Finds all customer IDs that the script could process. For a single account,
 * this is simply the account itself.
 *
 * @return {Array.<string>} A list of customer IDs.
 */
function getCustomerIdsPopulation() {
  if (IS_MANAGER) {
    var customerIds = [];

    var selector = MccApp.accounts();
    var conditions = TEMPLATE_CONFIG.ACCOUNT_CONDITIONS || [];
    for (var i = 0; i < conditions.length; i++) {
      selector = selector.withCondition(conditions[i]);
    }

    var accounts = selector.get();
    while (accounts.hasNext()) {
      customerIds.push(accounts.next().getCustomerId());
    }

    return customerIds;
  } else {
    return [AdWordsApp.currentAccount().getCustomerId()];
  }
}

/**
 * Returns the number of days between two dates.
 *
 * @param {Object} from The older Date object.
 * @param {Object} to The newer (more recent) Date object.
 * @return {number} The number of days between the given dates (possibly
 *     fractional).
 */
function dayDifference(from, to) {
  return (to.getTime() - from.getTime()) / (24 * 3600 * 1000);
}

/**
 * Loads a JavaScript object previously saved as JSON to a file on Drive.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object was previously saved.
 * @return {Object} The JavaScript object, or null if the file was not found.
 */
function loadObject(filename) {
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    return null;
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    return JSON.parse(file.getBlob().getDataAsString());
  }
}

/**
 * Saves a JavaScript object as JSON to a file on Drive. An existing file with
 * the same name is overwritten.
 *
 * @param {string} filename The name of the file in the account's root Drive
 *     folder where the object should be saved.
 * @param {obj} obj The object to save.
 */
function saveObject(filename, obj) {
  var files = DriveApp.getRootFolder().getFilesByName(filename);

  if (!files.hasNext()) {
    DriveApp.createFile(filename, JSON.stringify(obj));
  } else {
    var file = files.next();

    if (files.hasNext()) {
      throwDuplicateFileException(filename);
    }

    file.setContent(JSON.stringify(obj));
  }
}

/**
 * Throws an exception if there are multiple files with the same name.
 *
 * @param {string} filename The filename that caused the error.
 */
function throwDuplicateFileException(filename) {
  throw 'Multiple files named ' + filename + ' detected. Please ensure ' +
      'there is only one file named ' + filename + ' and try again.';
}

var stateManager = (function() {
  /**
   * @type {{
   *   cycle: {
   *     status: string,
   *     lastUpdate: string,
   *     startTime: string
   *   },
   *   accounts: Object.<string, {
   *     status: string,
   *     lastUpdate: string,
   *     returnValue: Object
   *   }>
   * }}
   */
  var state;

  /**
   * Loads the saved state of the script. If there is no previously
   * saved state, sets the state to an initial default.
   */
  var loadState = function() {
    state = loadObject(TEMPLATE_CONFIG.FILENAME);
    if (!state) {
      resetState();
    }
  };

  /**
   * Saves the state of the script to Drive.
   */
  var saveState = function() {
    saveObject(TEMPLATE_CONFIG.FILENAME, state);
  };

  /**
   * Resets the state to an initial default.
   */
  var resetState = function() {
    state = {};
    var date = Date();

    state.cycle = {
      status: Statuses.NOT_STARTED,
      lastUpdate: date,
      startTime: date
    };

    state.accounts = {};
    var customerIds = getCustomerIdsPopulation();

    for (var i = 0; i < customerIds.length; i++) {
      state.accounts[customerIds[i]] = {
        status: Statuses.NOT_STARTED,
        lastUpdate: date
      };
    }
  };

  /**
   * Gets the status of the current cycle.
   *
   * @return {string} The status of the current cycle.
   */
  var getStatus = function() {
    return state.cycle.status;
  };

  /**
   * Sets the status of the current cycle.
   *
   * @param {string} status The status of the current cycle.
   */
  var setStatus = function(status) {
    var date = Date();

    if (status == Statuses.IN_PROGRESS &&
        state.cycle.status == Statuses.NOT_STARTED) {
      state.cycle.startTime = date;
    }

    state.cycle.status = status;
    state.cycle.lastUpdate = date;
  };

  /**
   * Gets the start time of the current cycle.
   *
   * @return {Object} Date object for the start of the last cycle.
   */
  var getLastStartTime = function() {
    return new Date(state.cycle.startTime);
  };

  /**
   * Gets accounts in the current cycle with a particular status.
   *
   * @param {string} status The status of the accounts to get.
   *     If null, all accounts are retrieved.
   * @return {Array.<string>} A list of matching customerIds.
   */
  var getAccountsWithStatus = function(status) {
    var customerIds = [];

    for (var customerId in state.accounts) {
      if (!status || state.accounts[customerId].status == status) {
        customerIds.push(customerId);
      }
    }

    return customerIds;
  };

  /**
   * Sets accounts in the current cycle with a particular status.
   *
   * @param {Array.<string>} customerIds A list of customerIds.
   * @param {string} status A status to apply to those customerIds.
   */
  var setAccountsWithStatus = function(customerIds, status) {
    var date = Date();

    for (var i = 0; i < customerIds.length; i++) {
      var customerId = customerIds[i];

      if (state.accounts[customerId]) {
        state.accounts[customerId].status = status;
        state.accounts[customerId].lastUpdate = date;
      }
    }
  };

  /**
   * Registers the processing of a particular account with a result.
   *
   * @param {string} customerId The account that was processed.
   * @param {{
   *       status: string,
   *       returnValue: Object
   *       error: string
   *     }} result The object to save for that account.
   */
  var setAccountWithResult = function(customerId, result) {
    if (state.accounts[customerId]) {
      state.accounts[customerId].status = result.status;
      state.accounts[customerId].returnValue = result.returnValue;
      state.accounts[customerId].error = result.error;
      state.accounts[customerId].lastUpdate = Date();
    }
  };

  /**
   * Gets the current results of the cycle for all accounts.
   *
   * @return {Object.<string, {
   *       status: string,
   *       lastUpdate: string,
   *       returnValue: Object,
   *       error: string
   *     }>} The results processed by the script during the cycle,
   *    keyed by account.
   */
  var getResults = function() {
    return state.accounts;
  };

  return {
    loadState: loadState,
    saveState: saveState,
    resetState: resetState,
    getStatus: getStatus,
    setStatus: setStatus,
    getLastStartTime: getLastStartTime,
    getAccountsWithStatus: getAccountsWithStatus,
    setAccountsWithStatus: setAccountsWithStatus,
    setAccountWithResult: setAccountWithResult,
    getResults: getResults
  };
})();

/***************** END OF STANDARD TEMPLATE *****************/

发送以下问题的反馈:

此网页
AdWords Scripts
AdWords Scripts
需要帮助?请访问我们的支持页面