cache for store chard

This commit is contained in:
Timi
2026-08-24 18:32:48 +08:00
parent 6235cb3e3d
commit fe5d190308
8 changed files with 102 additions and 23 deletions
+29 -9
View File
@@ -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> {
+15 -1
View File
@@ -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">
+51
View File
@@ -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();
}