使用享元模式分批调用多个接口

196 阅读4分钟

需求

根据年份,查询优惠券作废记录

背景

6年前的比较老旧的项目,让前端根据新的UI设计重构并嵌入至新的APP,后端接口不变,前端按照新的交互自己想办法。

老系统的交互:根据月份查询当月数据,所以后端接口提供了三个接口,根据月份查询当月发券统计信息、根据月份查询当月券作废统计信息、根据月份查询作废券列表。

重构后交付:直接按照年份查询,页面根据月份倒序展示统计数据和作废券列表数据

问题

  1. 老系统根据月份,切换月份调用三个接口获取所需数据

  2. 改完之后按照年份查询,就造成了直接查询12个月的数据,没一个月又要调用三个接口,所以也就是要想一次获取一年的数据需要调用36个接口,一次性发送36个ajax请求,想想都可怕!!!!!!(造成网络拥堵、服务器压力大、性能问题都是不容小觑的)

解决方案

使用享元模式,对任务分批处理,实现产品需求。

享元模式介绍

享元模式主要用于减少大量相似对象的内存开销,通过共享可复用的对象来优化资源。可适用于与任务批量处理、文件批量上传等需求场景,在文件上传场景中,如果多个文件上传请求具有相同的配置(如上传地址、headers、参数等),那么这些“共享状态”可以作为内部状态提取出来,不同的文件信息(如文件名、内容)作为外部状态传入。

任务批量处理核心数据

接下来我们继续说我们用享元模式实现调用大批量接口的方法,一下是核心代码示例:

具体查询事件的构造函数:query.ts

import { getCouponInvalidSummationRequest, getCouponInvalidRecordRequest } from '@/api/modules/coupon';
import type { TypeMerchantBill, TypeQueryInvalidationRecordResult, TypeQueryRecordParams } from '@/types';

// 具体查询事件
export class Query {
  public _isUsing: boolean;

  public _commonParams: TypeQueryRecordParams = {}; // 公共查询参数,应该作为共享状态提取出来

  constructor(params: TypeQueryRecordParams) {
    this._commonParams = params;
    this._isUsing = false; // 控制改查询器是否仍在使用中
  }

  public search(month: string) {
    return new Promise<TypeQueryInvalidationRecordResult>((resolve) => {
      this._isUsing = true;

      const p1 = getCouponInvalidSummationRequest({ ...this._commonParams, month });
      const p2 = getCouponInvalidRecordRequest({
        ...this._commonParams, month
      });
      Promise.all([p1, p2]).then(([p1Res, p2Res]) => {
        this._isUsing = false;
        resolve({
          code: 0,
          month,
          invalidSum: p1Res?.data,
          invalidRecord: p2Res?.data?.data || [],
        });
      }).catch(() => {
        this._isUsing = false;
        resolve({
          code: 500,
          month,
          invalidSum: {} as TypeMerchantBill,
          invalidRecord: [],
        });
      });
    });
  }
}

查询器池子的代码:query-pool.ts

/*
 * // 最终期望得到的数据格式
 * const yearMonthData = {
 *   '2025-05': {
 *     code: 0, // 0:成功,500:失败
 *     month: '2025-05',
 *     invalidSum: {
 *       count: 0,
 *       totalMoney: 0,
 *       totalTime: 0,
 *     },
 *     invalidRecord: [
 *       {
 *         couponName: '',
 *         couponType: 1,
 *         couponUnit: 0,
 *         couponCount: 0,
 *         couponTotalAmount: 0,
 *         invalidTime: '',
 *       }
 *     ],
 *   },
 * };
 */
import { divide, subtract } from '@/utils/compute';
import type { TypeQueryInvalidationRecordResult, TypeQueryResult, TypeQueryRecordParams } from '@/types';
import { Query } from './query';

export class QueryPool {
  public _maxQuery: number = 3; // 一次最多发送多少次请求

  public _pool: Query[] = []; // 查询器池子

  public _queryMonthList: string[] = []; // 除去查询中的月份列表,此时没有查询器可使用了,所以先存下来,也就是在等待的月份列表。

  public _errorQueryMonthList: string[] = []; // 查询失败的月份列表

  public _monthList: string[] = []; // 需要查询的月份列表

  public _queryResult: TypeQueryResult = {}; // 最终查询结果

  public _commonParams: TypeQueryRecordParams = {}; // 公共查询参数,应该作为共享状态提取出来

  constructor(params: TypeQueryRecordParams) {
    this._commonParams = params;
    this.initQuery();
  }

  /**
   * @description: 注册查询器
   */
  public initQuery() {
    for (let i = 0; i < this._maxQuery; i++) {
      this._pool.push(new Query(this._commonParams));
    }
  }

  /**
   * @description: 获取一个可用的查询器
   */
  public getQuery() {
    return this._pool.find((query) => !query._isUsing);
  }

  /**
   * @description: 根据月份查询具体数据
   * @param {string} month
   * @return {*}
   */
  public queryByMonth(month: string) {
    const query = this.getQuery();
    // 获取查询器,判断是否有查询器可用,如果有就查询,如果没有可用的查询器就先放到等待列表中
    if (query) {
      query.search(month).then((res: TypeQueryInvalidationRecordResult) => {
        if (res.code === 0) {
          this._queryResult[res.month] = { ...res }; // 记录查询结果

          this.queryNextMonth(); // 继续查询
        } else {
          this._errorQueryMonthList.push(month);

          this._queryResult[res.month] = { ...res };
          this.queryNextMonth(); // 继续查询
        }
      });
    } else {
      this._queryMonthList.push(month);
    }
  }

  /**
   * @description: 检查是否有等待查询的月份,如果有就继续查询数据
   */
  public queryNextMonth() {
    const nextMonth = this._queryMonthList.length && this._queryMonthList.shift();
    if (nextMonth) {
      this.queryByMonth(nextMonth);
    }
  }

  /**
   * @description: 携带参数发起查询请求
   */
  public queryRequest(monthList: string[]) {
    this._monthList = monthList;
    monthList.forEach((month) => this.queryByMonth(month));
  }

  /**
   * @description: 获取查询进度
   */
  public getProcess() {
    const resultKeys = Object.keys(this._queryResult);
    const process = resultKeys.length === this._monthList.length ? 1 : divide(subtract(this._monthList.length, resultKeys.length), this._monthList.length);

    return process;
  }

  /**
   * @description: 获取查询结果
   */
  public getQueryResult() {
    return this._queryResult;
  }
}

页面中使用:

<script lang="ts" setup>
  import { ref } from 'vue';
  import { useRoute } from 'vue-router';
  import SelectDate from '@/components/select-date/index.vue';
  import type { TypeQueryResult } from '@/types';
  import { useUserStore } from '@/store/user';
  import { pinia } from '@/store/index';
  import { QueryPool } from './query-pool';

  type Interval = number | null
  
  const route = useRoute();

  const userStore = useUserStore(pinia);
  const commonParams = {
    merchantId: route?.query?.merchantId,
    merchantName: route?.query?.merchantName,
    lmToken: userStore.token,
    bu_code: userStore.buCode,
    channel: userStore.channel,
    projectId: userStore.projectId,
  };

  const currentYear = ref(`${new Date().getFullYear()}`);
  const currentYearMonthList = ref<string[]>([]);

  // 根据年份获取所有月份列表,不能超过当前月份
  const getMonthsByYear = (year: number): string[] => {
    if (year === new Date().getFullYear()) {
      return Array.from({ length: new Date().getMonth() + 1 }, (_, i) => `${year}-${String(i + 1).padStart(2, '0')}`);
    }
    return Array.from({ length: 12 }, (_, i) => `${year}-${String(i + 1).padStart(2, '0')}`);
  };

  const loading = ref(false);
  const recordData = ref<TypeQueryResult>({});
  let myInterval:Interval = null;
  const handleSearchAll = () => {
    const monthList = getMonthsByYear(Number(currentYear.value));
    currentYearMonthList.value = monthList;

    const query = new QueryPool(commonParams);
    query.queryRequest(monthList);
    loading.value = true;
    myInterval = window.setInterval(() => {
      const process = query.getProcess();
      if (process === 1) {
        loading.value = false;
        recordData.value = query.getQueryResult();
        if (myInterval) {
          window.clearInterval(myInterval);
          myInterval = null;
        }
      }
    }, 500);
  };

  handleSearchAll();
</script>