cache for store chard
This commit is contained in:
@@ -1,30 +1,48 @@
|
||||
import { axios } from "timi-web";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreAPI from "@/api/StoreAPI";
|
||||
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
||||
import type { StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView, StoreEventRecordNumberValueDailyStatView } from "@/types/Store";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type { StoreBusinessDay, StoreChartReq, StoreEventRecordCalendarView, StoreEventRecordDailyStatView, StoreEventRecordNumberValueDailyStatView } from "@/types/Store";
|
||||
import { getStoreChartCache } from "@/utils/storeChartCache";
|
||||
import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store";
|
||||
|
||||
const BASE_URI = "/gao/store/chart";
|
||||
|
||||
async function customerDailyStat(req: StoreChartReq): Promise<CustomerDailyStatView[]> {
|
||||
return await axios.post(`${BASE_URI}/customer/daily`, withCurrentStoreId(req));
|
||||
const request = withCurrentStoreId(req);
|
||||
return await getStoreChartCache(`customer-daily:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/customer/daily`, request));
|
||||
}
|
||||
|
||||
async function customerGenderStat(): Promise<CustomerGenderStatView[]> {
|
||||
return await axios.post(`${BASE_URI}/customer/gender`, undefined, {
|
||||
params: { storeId: getCurrentStoreId() }
|
||||
});
|
||||
const storeId = getCurrentStoreId();
|
||||
return await getStoreChartCache(`customer-gender:${storeId}`, async () => await axios.post(`${BASE_URI}/customer/gender`, undefined, {
|
||||
params: { storeId }
|
||||
}));
|
||||
}
|
||||
|
||||
async function eventRecordDailyStat(req: StoreChartReq): Promise<StoreEventRecordDailyStatView[]> {
|
||||
return await axios.post(`${BASE_URI}/event/record/daily`, withCurrentStoreId(req));
|
||||
const request = withCurrentStoreId(req);
|
||||
return await getStoreChartCache(`event-record-daily:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/event/record/daily`, request));
|
||||
}
|
||||
|
||||
async function eventRecordNumberValueDailyStat(req: StoreChartReq): Promise<StoreEventRecordNumberValueDailyStatView[]> {
|
||||
return await axios.post(`${BASE_URI}/event/record/number-value/daily`, withCurrentStoreId(req));
|
||||
const request = withCurrentStoreId(req);
|
||||
return await getStoreChartCache(`event-record-number-value-daily:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/event/record/number-value/daily`, request));
|
||||
}
|
||||
|
||||
async function eventRecordCalendar(req: StoreChartReq): Promise<StoreEventRecordCalendarView[]> {
|
||||
return await axios.post(`${BASE_URI}/event/record/calendar`, withCurrentStoreId(req));
|
||||
const request = withCurrentStoreId(req);
|
||||
return await getStoreChartCache(`event-record-calendar:${JSON.stringify(request)}`, async () => await axios.post(`${BASE_URI}/event/record/calendar`, request));
|
||||
}
|
||||
|
||||
async function listValidEvents(): Promise<EventItem[]> {
|
||||
const storeId = getCurrentStoreId();
|
||||
return await getStoreChartCache(`valid-events:${storeId}`, async () => await EventAPI.listValid());
|
||||
}
|
||||
|
||||
async function businessDayCalendar(storeId: string, beginDateValue: number, endDateValue: number): Promise<StoreBusinessDay[]> {
|
||||
return await getStoreChartCache(`business-day-calendar:${storeId}:${beginDateValue}:${endDateValue}`, async () => await StoreAPI.businessDayCalendar(storeId, beginDateValue, endDateValue));
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -32,5 +50,7 @@ export default {
|
||||
customerGenderStat,
|
||||
eventRecordDailyStat,
|
||||
eventRecordNumberValueDailyStat,
|
||||
eventRecordCalendar
|
||||
eventRecordCalendar,
|
||||
listValidEvents,
|
||||
businessDayCalendar
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import type { CustomerDailyStatView } from "@/types/Customer";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
@@ -63,7 +62,7 @@ async function loadSummaryData(): Promise<void> {
|
||||
}
|
||||
|
||||
const requestId = ++summaryRequestId;
|
||||
eventList.value = await EventAPI.listValid();
|
||||
eventList.value = await StoreChartAPI.listValidEvents();
|
||||
const eventIdList = getEventIdList();
|
||||
const [customerStatList, recordDailyStatList] = await Promise.all([
|
||||
StoreChartAPI.customerDailyStat({
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import EventRecordCalendar, { type EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
@@ -44,7 +43,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
async function loadEvents(): Promise<void> {
|
||||
eventList.value = await EventAPI.listValid();
|
||||
eventList.value = await StoreChartAPI.listValidEvents();
|
||||
const eventIdList = getEventIdList();
|
||||
selectedEventIdList.value = selectedEventIdList.value.length ? selectedEventIdList.value.filter(item => eventIdList.includes(item)) : eventIdList;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import type { EChartsOption } from "echarts";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
@@ -107,7 +106,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
async function loadEvents(): Promise<void> {
|
||||
numberEventList.value = (await EventAPI.listValid()).filter(item => item.valueType === "NUMBER");
|
||||
numberEventList.value = (await StoreChartAPI.listValidEvents()).filter(item => item.valueType === "NUMBER");
|
||||
if (!selectedEventId.value) {
|
||||
selectedEventId.value = numberEventList.value[0]?.id;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import type { EChartsOption, MarkAreaComponentOption } from "echarts";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreAPI from "@/api/StoreAPI";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
@@ -131,7 +129,7 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
async function loadEvents(): Promise<void> {
|
||||
eventList.value = await EventAPI.listValid();
|
||||
eventList.value = await StoreChartAPI.listValidEvents();
|
||||
}
|
||||
|
||||
async function loadDailyStat(): Promise<void> {
|
||||
@@ -203,7 +201,7 @@ async function loadRestDays(): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
const list = await StoreAPI.businessDayCalendar(storeId, getDateValue(dateList[0]), getDateValue(dateList[dateList.length - 1]));
|
||||
const list = await StoreChartAPI.businessDayCalendar(storeId, getDateValue(dateList[0]), getDateValue(dateList[dateList.length - 1]));
|
||||
if (requestId === restDayRequestId) {
|
||||
restDaySet.value = new Set(getRestDayList(list));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Time } from "timi-web";
|
||||
import type { EChartsOption } from "echarts";
|
||||
import EventAPI from "@/api/EventAPI";
|
||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import type { StoreEventRecordDailyStatView } from "@/types/Store";
|
||||
@@ -111,7 +110,7 @@ onBeforeUnmount(() => {
|
||||
});
|
||||
|
||||
async function loadEvents(): Promise<void> {
|
||||
eventList.value = await EventAPI.listValid();
|
||||
eventList.value = await StoreChartAPI.listValidEvents();
|
||||
}
|
||||
|
||||
async function loadDailyStat(): Promise<void> {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
</section>
|
||||
<store-data-charts :key="currentStoreStore.currentStoreId" />
|
||||
</section>
|
||||
<store-data-charts :key="`${currentStoreStore.currentStoreId}-${chartRefreshKey}`" />
|
||||
<t-popup v-model="storePickerVisible" placement="bottom">
|
||||
<t-picker
|
||||
v-model="storePickerDraft"
|
||||
@@ -53,7 +53,9 @@
|
||||
import { Message } from "@/utils/message";
|
||||
import { useUserStore } from "timi-web";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission";
|
||||
import { clearStoreChartCache } from "@/utils/storeChartCache";
|
||||
|
||||
type PickerValue = string | number;
|
||||
|
||||
@@ -66,6 +68,8 @@ defineOptions({
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const navBarStore = useNavBarStore();
|
||||
const chartRefreshKey = ref(0);
|
||||
|
||||
const currentStore = computed(() => currentStoreStore.currentStore);
|
||||
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||
@@ -112,6 +116,11 @@ function confirmStorePicker(): void {
|
||||
Message.success("当前门店已切换");
|
||||
}
|
||||
|
||||
function refreshCharts(): void {
|
||||
clearStoreChartCache();
|
||||
chartRefreshKey.value++;
|
||||
}
|
||||
|
||||
// ---------- 门店管理 ----------
|
||||
|
||||
async function goStoreManagement(): Promise<void> {
|
||||
@@ -130,12 +139,17 @@ async function goStoreManagement(): Promise<void> {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
navBarStore.setRightIcon("refresh", refreshCharts);
|
||||
if (canSelectStore.value) {
|
||||
await currentStoreStore.loadStoreList();
|
||||
} else if (canLoadLoginStore.value) {
|
||||
await currentStoreStore.loadLoginStore();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
navBarStore.clearRight();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
const STORE_CHART_CACHE_TTL = 5 * 60 * 1000;
|
||||
|
||||
type StoreChartCacheEntry = {
|
||||
data?: unknown;
|
||||
cachedAt: number;
|
||||
pending?: Promise<unknown>;
|
||||
};
|
||||
|
||||
const cache = new Map<string, StoreChartCacheEntry>();
|
||||
let cacheGeneration = 0;
|
||||
|
||||
/** 缓存门店图表请求结果,并合并同一数据的并发请求。 */
|
||||
export async function getStoreChartCache<T>(key: string, loader: () => Promise<T>): Promise<T> {
|
||||
const now = Date.now();
|
||||
const entry = cache.get(key);
|
||||
if (entry?.data !== undefined && now - entry.cachedAt < STORE_CHART_CACHE_TTL) {
|
||||
return entry.data as T;
|
||||
}
|
||||
if (entry?.pending) {
|
||||
return await entry.pending as T;
|
||||
}
|
||||
|
||||
const generation = cacheGeneration;
|
||||
const pending = loader();
|
||||
cache.set(key, {
|
||||
data: entry?.data,
|
||||
cachedAt: entry?.cachedAt || 0,
|
||||
pending
|
||||
});
|
||||
try {
|
||||
const data = await pending;
|
||||
if (generation === cacheGeneration) {
|
||||
cache.set(key, {
|
||||
data,
|
||||
cachedAt: Date.now()
|
||||
});
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (cache.get(key)?.pending === pending) {
|
||||
cache.delete(key);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空门店图表缓存,并使清空前发起的请求不再回填缓存。 */
|
||||
export function clearStoreChartCache(): void {
|
||||
cacheGeneration++;
|
||||
cache.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user