需求
根据年份,查询优惠券作废记录
背景
6年前的比较老旧的项目,让前端根据新的UI设计重构并嵌入至新的APP,后端接口不变,前端按照新的交互自己想办法。
老系统的交互:根据月份查询当月数据,所以后端接口提供了三个接口,根据月份查询当月发券统计信息、根据月份查询当月券作废统计信息、根据月份查询作废券列表。
重构后交付:直接按照年份查询,页面根据月份倒序展示统计数据和作废券列表数据
问题
-
老系统根据月份,切换月份调用三个接口获取所需数据
-
改完之后按照年份查询,就造成了直接查询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>