cache for store chard
This commit is contained in:
@@ -1,30 +1,48 @@
|
|||||||
import { axios } from "timi-web";
|
import { axios } from "timi-web";
|
||||||
|
import EventAPI from "@/api/EventAPI";
|
||||||
|
import StoreAPI from "@/api/StoreAPI";
|
||||||
import type { CustomerDailyStatView, CustomerGenderStatView } from "@/types/Customer";
|
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";
|
import { getCurrentStoreId, withCurrentStoreId } from "@/utils/store";
|
||||||
|
|
||||||
const BASE_URI = "/gao/store/chart";
|
const BASE_URI = "/gao/store/chart";
|
||||||
|
|
||||||
async function customerDailyStat(req: StoreChartReq): Promise<CustomerDailyStatView[]> {
|
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[]> {
|
async function customerGenderStat(): Promise<CustomerGenderStatView[]> {
|
||||||
return await axios.post(`${BASE_URI}/customer/gender`, undefined, {
|
const storeId = getCurrentStoreId();
|
||||||
params: { 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[]> {
|
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[]> {
|
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[]> {
|
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 {
|
export default {
|
||||||
@@ -32,5 +50,7 @@ export default {
|
|||||||
customerGenderStat,
|
customerGenderStat,
|
||||||
eventRecordDailyStat,
|
eventRecordDailyStat,
|
||||||
eventRecordNumberValueDailyStat,
|
eventRecordNumberValueDailyStat,
|
||||||
eventRecordCalendar
|
eventRecordCalendar,
|
||||||
|
listValidEvents,
|
||||||
|
businessDayCalendar
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import EventAPI from "@/api/EventAPI";
|
|
||||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||||
import type { CustomerDailyStatView } from "@/types/Customer";
|
import type { CustomerDailyStatView } from "@/types/Customer";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
@@ -63,7 +62,7 @@ async function loadSummaryData(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestId = ++summaryRequestId;
|
const requestId = ++summaryRequestId;
|
||||||
eventList.value = await EventAPI.listValid();
|
eventList.value = await StoreChartAPI.listValidEvents();
|
||||||
const eventIdList = getEventIdList();
|
const eventIdList = getEventIdList();
|
||||||
const [customerStatList, recordDailyStatList] = await Promise.all([
|
const [customerStatList, recordDailyStatList] = await Promise.all([
|
||||||
StoreChartAPI.customerDailyStat({
|
StoreChartAPI.customerDailyStat({
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import EventAPI from "@/api/EventAPI";
|
|
||||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||||
import EventRecordCalendar, { type EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
import EventRecordCalendar, { type EventRecordCalendarListPayload } from "@/components/EventRecordCalendar.vue";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
@@ -44,7 +43,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadEvents(): Promise<void> {
|
async function loadEvents(): Promise<void> {
|
||||||
eventList.value = await EventAPI.listValid();
|
eventList.value = await StoreChartAPI.listValidEvents();
|
||||||
const eventIdList = getEventIdList();
|
const eventIdList = getEventIdList();
|
||||||
selectedEventIdList.value = selectedEventIdList.value.length ? selectedEventIdList.value.filter(item => eventIdList.includes(item)) : eventIdList;
|
selectedEventIdList.value = selectedEventIdList.value.length ? selectedEventIdList.value.filter(item => eventIdList.includes(item)) : eventIdList;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import type { EChartsOption } from "echarts";
|
import type { EChartsOption } from "echarts";
|
||||||
import EventAPI from "@/api/EventAPI";
|
|
||||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||||
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
@@ -107,7 +106,7 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadEvents(): Promise<void> {
|
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) {
|
if (!selectedEventId.value) {
|
||||||
selectedEventId.value = numberEventList.value[0]?.id;
|
selectedEventId.value = numberEventList.value[0]?.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import type { EChartsOption, MarkAreaComponentOption } from "echarts";
|
import type { EChartsOption, MarkAreaComponentOption } from "echarts";
|
||||||
import EventAPI from "@/api/EventAPI";
|
|
||||||
import StoreAPI from "@/api/StoreAPI";
|
|
||||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||||
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
import ChartRangeSlider from "@/components/ChartRangeSlider.vue";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
@@ -131,7 +129,7 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadEvents(): Promise<void> {
|
async function loadEvents(): Promise<void> {
|
||||||
eventList.value = await EventAPI.listValid();
|
eventList.value = await StoreChartAPI.listValidEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDailyStat(): Promise<void> {
|
async function loadDailyStat(): Promise<void> {
|
||||||
@@ -203,7 +201,7 @@ async function loadRestDays(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
if (requestId === restDayRequestId) {
|
||||||
restDaySet.value = new Set(getRestDayList(list));
|
restDaySet.value = new Set(getRestDayList(list));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
import type { EChartsOption } from "echarts";
|
import type { EChartsOption } from "echarts";
|
||||||
import EventAPI from "@/api/EventAPI";
|
|
||||||
import StoreChartAPI from "@/api/StoreChartAPI";
|
import StoreChartAPI from "@/api/StoreChartAPI";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import type { StoreEventRecordDailyStatView } from "@/types/Store";
|
import type { StoreEventRecordDailyStatView } from "@/types/Store";
|
||||||
@@ -111,7 +110,7 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadEvents(): Promise<void> {
|
async function loadEvents(): Promise<void> {
|
||||||
eventList.value = await EventAPI.listValid();
|
eventList.value = await StoreChartAPI.listValidEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDailyStat(): Promise<void> {
|
async function loadDailyStat(): Promise<void> {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
</t-cell>
|
</t-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
</section>
|
</section>
|
||||||
<store-data-charts :key="currentStoreStore.currentStoreId" />
|
<store-data-charts :key="`${currentStoreStore.currentStoreId}-${chartRefreshKey}`" />
|
||||||
<t-popup v-model="storePickerVisible" placement="bottom">
|
<t-popup v-model="storePickerVisible" placement="bottom">
|
||||||
<t-picker
|
<t-picker
|
||||||
v-model="storePickerDraft"
|
v-model="storePickerDraft"
|
||||||
@@ -53,7 +53,9 @@
|
|||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { useUserStore } from "timi-web";
|
import { useUserStore } from "timi-web";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, gaoStoreScopePermissionCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
import { clearStoreChartCache } from "@/utils/storeChartCache";
|
||||||
|
|
||||||
type PickerValue = string | number;
|
type PickerValue = string | number;
|
||||||
|
|
||||||
@@ -66,6 +68,8 @@ defineOptions({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
|
const navBarStore = useNavBarStore();
|
||||||
|
const chartRefreshKey = ref(0);
|
||||||
|
|
||||||
const currentStore = computed(() => currentStoreStore.currentStore);
|
const currentStore = computed(() => currentStoreStore.currentStore);
|
||||||
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
const canSelectStore = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||||
@@ -112,6 +116,11 @@ function confirmStorePicker(): void {
|
|||||||
Message.success("当前门店已切换");
|
Message.success("当前门店已切换");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshCharts(): void {
|
||||||
|
clearStoreChartCache();
|
||||||
|
chartRefreshKey.value++;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 门店管理 ----------
|
// ---------- 门店管理 ----------
|
||||||
|
|
||||||
async function goStoreManagement(): Promise<void> {
|
async function goStoreManagement(): Promise<void> {
|
||||||
@@ -130,12 +139,17 @@ async function goStoreManagement(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
navBarStore.setRightIcon("refresh", refreshCharts);
|
||||||
if (canSelectStore.value) {
|
if (canSelectStore.value) {
|
||||||
await currentStoreStore.loadStoreList();
|
await currentStoreStore.loadStoreList();
|
||||||
} else if (canLoadLoginStore.value) {
|
} else if (canLoadLoginStore.value) {
|
||||||
await currentStoreStore.loadLoginStore();
|
await currentStoreStore.loadLoginStore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
navBarStore.clearRight();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<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