diff --git a/docs/bszx-统计数据状态管理修复总结.md b/docs/bszx-统计数据状态管理修复总结.md new file mode 100644 index 0000000..766fca8 --- /dev/null +++ b/docs/bszx-统计数据状态管理修复总结.md @@ -0,0 +1,224 @@ +# 百色中学统计数据状态管理修复总结 + +## 问题背景 + +在 `src/views/bszx/dashboard/index.vue` 中,`getTotalBszxPrice` 函数存在以下问题: + +1. **异步函数在计算属性中使用错误**: + ```typescript + const totalBszxPrice = computed(() => getTotalBszxPrice()); // ❌ 返回 Promise 而不是数值 + ``` + +2. **TypeScript 类型错误**:IDE 提示 "Object is possibly undefined" + +3. **数据不统一**:`totalPriceAmount` 在多个组件中重复计算和传递 + +## 解决方案 + +### 1. 创建专门的百色中学统计数据 Store + +**文件位置**:`src/store/modules/bszx-statistics.ts` + +**核心功能**: +- 统一管理百色中学相关的统计数据 +- 智能缓存机制(5分钟有效期) +- 自动刷新功能 +- 完整的类型保护和错误处理 + +**主要特性**: +```typescript +export const useBszxStatisticsStore = defineStore({ + id: 'bszx-statistics', + state: (): BszxStatisticsState => ({ + totalPrice: 0, + loading: false, + lastUpdateTime: null, + cacheExpiry: 5 * 60 * 1000, // 5分钟缓存 + refreshTimer: null + }), + + getters: { + bszxTotalPrice: (state): number => safeNumber(state.totalPrice) + }, + + actions: { + async fetchBszxStatistics(forceRefresh = false), + startAutoRefresh(interval = 5 * 60 * 1000), + stopAutoRefresh() + } +}); +``` + +### 2. 修复 Dashboard 页面 + +**修复前**: +```typescript +// ❌ 错误的实现 +const totalBszxPrice = computed(() => getTotalBszxPrice()); +const getTotalBszxPrice = async () => { + return await bszxOrderTotal() +} +``` + +**修复后**: +```typescript +// ✅ 正确的实现 +const bszxStatisticsStore = useBszxStatisticsStore(); +const totalBszxPrice = computed(() => bszxStatisticsStore.bszxTotalPrice); + +onMounted(async () => { + await Promise.all([ + siteStore.fetchSiteInfo(), + statisticsStore.fetchStatistics(), + bszxStatisticsStore.fetchBszxStatistics() // 加载百色中学统计数据 + ]); + + statisticsStore.startAutoRefresh(); + bszxStatisticsStore.startAutoRefresh(); // 开始自动刷新 +}); +``` + +### 3. 统一 totalPriceAmount 的使用 + +**涉及的文件**: +- `src/views/bszx/bszxPayRanking/index.vue` +- `src/views/bszx/bszxPayRanking/components/search.vue` +- `src/views/bsyx/bsyxPayRanking/index.vue` +- `src/views/bsyx/bsyxPayRanking/components/search.vue` + +**修复策略**: +1. **Search 组件**:直接从 store 获取数据 + ```typescript + // 使用百色中学统计数据 store + const bszxStatisticsStore = useBszxStatisticsStore(); + const bszxTotalPrice = computed(() => bszxStatisticsStore.bszxTotalPrice); + ``` + +2. **主组件**:更新 store 数据而不是本地变量 + ```typescript + const datasource: DatasourceFunction = ({where}) => { + return ranking({...where}).then(data => { + // 计算总金额并更新到 store + let totalPrice = 0; + data.forEach((item) => { + if (item.totalPrice) { + totalPrice += item.totalPrice; + } + }); + + // 更新 store 中的数据 + bszxStatisticsStore.updateStatistics({ totalPrice }); + + return data; + }); + }; + ``` + +## 核心改进 + +### 1. 类型安全 +- 使用 `safeNumber` 工具函数确保数据类型安全 +- 完整的 TypeScript 类型定义 +- 运行时类型检查 + +### 2. 数据一致性 +- 统一的数据源(store) +- 避免重复计算和传递 +- 自动同步更新 + +### 3. 性能优化 +- 智能缓存机制(5分钟有效期) +- 自动刷新功能 +- 避免不必要的 API 调用 + +### 4. 错误处理 +- 完善的错误捕获和处理 +- 优雅的降级策略 +- 详细的错误日志 + +## 使用方式 + +### 在组件中使用 + +```typescript +import { useBszxStatisticsStore } from '@/store/modules/bszx-statistics'; + +const bszxStatisticsStore = useBszxStatisticsStore(); + +// 获取总金额 +const totalPrice = computed(() => bszxStatisticsStore.bszxTotalPrice); + +// 初始化数据 +onMounted(async () => { + await bszxStatisticsStore.fetchBszxStatistics(); + bszxStatisticsStore.startAutoRefresh(); // 开始自动刷新 +}); + +// 清理资源 +onUnmounted(() => { + bszxStatisticsStore.stopAutoRefresh(); +}); +``` + +### API 方法 + +```typescript +// 获取统计数据(带缓存) +await bszxStatisticsStore.fetchBszxStatistics(); + +// 强制刷新 +await bszxStatisticsStore.fetchBszxStatistics(true); + +// 更新数据 +bszxStatisticsStore.updateStatistics({ totalPrice: 1000 }); + +// 开始自动刷新(默认5分钟间隔) +bszxStatisticsStore.startAutoRefresh(); + +// 停止自动刷新 +bszxStatisticsStore.stopAutoRefresh(); + +// 清除缓存 +bszxStatisticsStore.clearCache(); +``` + +## 验证结果 + +✅ **TypeScript 编译通过** - 无类型错误 +✅ **生产构建成功** - 无运行时错误 +✅ **数据统一管理** - 避免重复计算 +✅ **类型安全** - 完整的类型保护 +✅ **性能优化** - 智能缓存和自动刷新 + +## 最佳实践 + +1. **生命周期管理**: + ```typescript + onMounted(() => bszxStatisticsStore.startAutoRefresh()); + onUnmounted(() => bszxStatisticsStore.stopAutoRefresh()); + ``` + +2. **错误处理**: + ```typescript + try { + await bszxStatisticsStore.fetchBszxStatistics(); + } catch (error) { + console.error('获取统计数据失败:', error); + } + ``` + +3. **强制刷新**: + ```typescript + const handleRefresh = () => bszxStatisticsStore.fetchBszxStatistics(true); + ``` + +## 总结 + +通过创建专门的 `bszx-statistics` store,我们成功解决了: + +1. **异步函数在计算属性中的错误使用** +2. **TypeScript 类型安全问题** +3. **数据重复计算和传递问题** +4. **缺乏统一的数据管理** + +这个实现提供了更好的类型安全性、数据一致性和性能优化,为百色中学相关功能提供了可靠的数据支撑。 diff --git a/docs/数据不一致问题修复说明.md b/docs/数据不一致问题修复说明.md new file mode 100644 index 0000000..930af85 --- /dev/null +++ b/docs/数据不一致问题修复说明.md @@ -0,0 +1,171 @@ +# 百色中学统计金额数据不一致问题修复说明 + +## 问题描述 + +用户发现 `/bszx/ranking` 页面的统计金额和 `/bszx/dashboard` 页面的统计金额不一致,需要确定哪个数据是正确的。 + +## 问题分析 + +### 数据来源差异 + +经过分析,发现两个页面使用了不同的数据源: + +#### 1. Dashboard 页面 (`/bszx/dashboard`) +- **API**: `bszxOrderTotal()` +- **接口**: `/bszx/bszx-order/total` +- **数据来源**: **订单表** (`bszx-order`) +- **数据类型**: `ShopOrder[]` - 订单数组 +- **统计逻辑**: 统计所有有效订单的实际支付金额 +- **字段**: `payPrice` 或 `totalPrice` + +#### 2. Ranking 页面 (`/bszx/ranking`) +- **API**: `ranking()` +- **接口**: `/bszx/bszx-pay-ranking/ranking` +- **数据来源**: **捐款排行表** (`bszx-pay-ranking`) +- **数据类型**: `BszxPayRanking[]` - 排行榜记录数组 +- **统计逻辑**: 统计排行榜中的汇总金额 +- **字段**: `totalPrice` + +### 数据性质差异 + +1. **订单表数据** (Dashboard): + - ✅ **真实的业务数据** + - ✅ 反映实际的支付情况 + - ✅ 只统计有效订单(已支付、未取消) + - ✅ 实时更新 + +2. **排行榜数据** (Ranking): + - ⚠️ **展示用的汇总数据** + - ⚠️ 可能包含统计逻辑或过滤条件 + - ⚠️ 可能不是实时更新 + - ⚠️ 用于排行榜展示,不一定等于实际订单金额 + +## 结论 + +**Dashboard 页面的数据是正确的**,因为: + +1. **数据权威性**:直接来自订单表,是真实的业务数据 +2. **统计准确性**:只统计有效订单的实际支付金额 +3. **实时性**:反映当前的真实订单状态 +4. **业务意义**:代表实际的收入情况 + +**Ranking 页面的数据是展示数据**,可能: +- 是为了排行榜展示而特别处理的数据 +- 包含不同的统计规则或过滤条件 +- 不代表实际的订单收入 + +## 修复方案 + +### 1. 修正数据处理逻辑 + +**修复前的问题**: +```typescript +// ❌ 错误:ranking 页面覆盖了真实的订单统计数据 +const datasource = ({where}) => { + return ranking({...where}).then(data => { + let totalPrice = 0; + data.forEach((item) => { + if(item.totalPrice) totalPrice += item.totalPrice; + }); + bszxStatisticsStore.updateStatistics({ totalPrice }); // 错误地覆盖了真实数据 + return data; + }); +}; +``` + +**修复后**: +```typescript +// ✅ 正确:分离两种数据,不相互覆盖 +const datasource = ({where}) => { + return ranking({...where}).then(data => { + // 计算排行榜总金额(仅用于本页面显示) + let total = 0; + data.forEach((item) => { + if(item.totalPrice) total += item.totalPrice; + }); + rankingTotalPrice.value = total; // 本地变量,不影响全局 store + + // store 中的数据来自 bszxOrderTotal API,代表真实的订单金额 + return data; + }); +}; +``` + +### 2. 优化 Store 数据处理 + +**修复 `bszxOrderTotal` 数据解析**: +```typescript +// 修复前:不正确的数据处理 +if (Array.isArray(result) && result.length > 0) { + totalPrice = safeNumber(result[0]); // ❌ 只取第一个元素 +} + +// 修复后:正确累加所有订单金额 +if (Array.isArray(result)) { + result.forEach((order: any) => { + if (order.payPrice) { + totalPrice += safeNumber(order.payPrice); // ✅ 累加实际支付金额 + } else if (order.totalPrice) { + totalPrice += safeNumber(order.totalPrice); // ✅ 备用字段 + } + }); +} +``` + +### 3. 用户界面优化 + +为了让用户清楚地了解两种数据的差异,在 Ranking 页面同时显示两个金额: + +```vue + + + 实际订单总金额: + ¥{{ formatNumber(bszxTotalPrice) }} + + + + + 排行榜统计金额: + ¥{{ formatNumber(rankingTotalPrice) }} + +``` + +## 修改的文件 + +### 1. Store 层面 +- `src/store/modules/bszx-statistics.ts` - 修正数据解析逻辑 + +### 2. Dashboard 页面 +- 无需修改,已经使用正确的数据源 + +### 3. Ranking 页面 +- `src/views/bszx/bszxPayRanking/index.vue` - 分离数据处理逻辑 +- `src/views/bszx/bszxPayRanking/components/search.vue` - 显示两种金额 +- `src/views/bsyx/bsyxPayRanking/index.vue` - 同样的修改 +- `src/views/bsyx/bsyxPayRanking/components/search.vue` - 同样的修改 + +## 验证方法 + +1. **检查 Dashboard 页面**:显示的是真实订单总金额 +2. **检查 Ranking 页面**:同时显示两种金额,用户可以对比 +3. **数据一致性**:Dashboard 和 Ranking 页面的"实际订单总金额"应该一致 +4. **数据差异说明**:两个金额可能不同,这是正常的,因为数据来源和统计逻辑不同 + +## 最佳实践 + +1. **数据权威性**:始终以订单表数据为准进行业务决策 +2. **数据透明性**:向用户清楚说明不同数据的来源和含义 +3. **避免混淆**:不同数据源的数据不应相互覆盖 +4. **文档说明**:为不同的统计数据提供清晰的说明 + +## 总结 + +通过这次修复: + +1. ✅ **明确了数据权威性**:订单表数据是权威数据源 +2. ✅ **分离了数据处理**:不同数据源不再相互干扰 +3. ✅ **提高了透明度**:用户可以看到两种数据的对比 +4. ✅ **保持了一致性**:全局 store 中的数据始终来自权威数据源 +5. ✅ **改善了用户体验**:清楚标注了数据来源和含义 + +现在用户可以清楚地看到两种数据,并理解它们的差异和用途。 diff --git a/src/api/bszx/bszxOrder/index.ts b/src/api/bszx/bszxOrder/index.ts index ecd79d7..4c7faff 100644 --- a/src/api/bszx/bszxOrder/index.ts +++ b/src/api/bszx/bszxOrder/index.ts @@ -18,3 +18,20 @@ export async function pageBszxOrder(params: ShopOrderParam) { } return Promise.reject(new Error(res.data.message)); } + + +/** + * 统计订单总金额(只统计有效订单) + */ +export async function bszxOrderTotal(params?: ShopOrderParam) { + const res = await request.get>( + MODULES_API_URL + '/bszx/bszx-order/total', + { + params + } + ); + if (res.data.code === 0 && res.data.data) { + return res.data.data; + } + return Promise.reject(new Error(res.data.message)); +} diff --git a/src/store/modules/bszx-statistics.ts b/src/store/modules/bszx-statistics.ts new file mode 100644 index 0000000..9cbc9c6 --- /dev/null +++ b/src/store/modules/bszx-statistics.ts @@ -0,0 +1,141 @@ +/** + * 百色中学统计数据 store + */ +import { defineStore } from 'pinia'; +import { bszxOrderTotal } from '@/api/bszx/bszxOrder'; +import { safeNumber } from '@/utils/type-guards'; + +export interface BszxStatisticsState { + // 总营业额 + totalPrice: number; + // 加载状态 + loading: boolean; + // 最后更新时间 + lastUpdateTime: number | null; + // 缓存有效期(毫秒)- 5分钟缓存 + cacheExpiry: number; + // 自动刷新定时器 + refreshTimer: number | null; +} + +export const useBszxStatisticsStore = defineStore({ + id: 'bszx-statistics', + state: (): BszxStatisticsState => ({ + totalPrice: 0, + loading: false, + lastUpdateTime: null, + // 默认缓存5分钟 + cacheExpiry: 5 * 60 * 1000, + refreshTimer: null + }), + + getters: { + /** + * 获取总营业额 + */ + bszxTotalPrice: (state): number => { + return safeNumber(state.totalPrice); + }, + + /** + * 检查缓存是否有效 + */ + isCacheValid: (state): boolean => { + if (!state.lastUpdateTime) return false; + const now = Date.now(); + return (now - state.lastUpdateTime) < state.cacheExpiry; + } + }, + + actions: { + /** + * 获取百色中学统计数据 + * @param forceRefresh 是否强制刷新 + */ + async fetchBszxStatistics(forceRefresh = false) { + // 如果缓存有效且不强制刷新,直接返回缓存数据 + if (!forceRefresh && this.isCacheValid && this.totalPrice > 0) { + return this.totalPrice; + } + + this.loading = true; + try { + const result = await bszxOrderTotal(); + + // 处理返回的数据 - bszxOrderTotal 返回 ShopOrder[] 数组 + let totalPrice = 0; + if (Array.isArray(result)) { + // 累加所有订单的金额 + result.forEach((order: any) => { + if (order.payPrice) { + totalPrice += safeNumber(order.payPrice); + } else if (order.totalPrice) { + totalPrice += safeNumber(order.totalPrice); + } + }); + } else if (typeof result === 'number') { + totalPrice = result; + } else if (typeof result === 'string') { + totalPrice = safeNumber(result); + } else if (result && typeof result === 'object' && 'totalPrice' in result) { + totalPrice = safeNumber((result as any).totalPrice); + } + + this.totalPrice = totalPrice; + this.lastUpdateTime = Date.now(); + + return totalPrice; + } catch (error) { + console.error('获取百色中学统计数据失败:', error); + // 发生错误时不重置现有数据,只记录错误 + throw error; + } finally { + this.loading = false; + } + }, + + /** + * 更新统计数据 + */ + updateStatistics(data: Partial) { + Object.assign(this, data); + this.lastUpdateTime = Date.now(); + }, + + /** + * 清除缓存 + */ + clearCache() { + this.totalPrice = 0; + this.lastUpdateTime = null; + }, + + /** + * 设置缓存有效期 + */ + setCacheExpiry(expiry: number) { + this.cacheExpiry = expiry; + }, + + /** + * 开始自动刷新 + * @param interval 刷新间隔(毫秒),默认5分钟 + */ + startAutoRefresh(interval = 5 * 60 * 1000) { + this.stopAutoRefresh(); + this.refreshTimer = window.setInterval(() => { + this.fetchBszxStatistics(true).catch(console.error); + }, interval); + }, + + /** + * 停止自动刷新 + */ + stopAutoRefresh() { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + } + } +}); diff --git a/src/views/bsyx/bsyxPayRanking/components/search.vue b/src/views/bsyx/bsyxPayRanking/components/search.vue index c35d491..e99b42d 100644 --- a/src/views/bsyx/bsyxPayRanking/components/search.vue +++ b/src/views/bsyx/bsyxPayRanking/components/search.vue @@ -6,26 +6,43 @@ @change="search" value-format="YYYY-MM-DD" /> - - 捐款总金额: - ¥{{ formatNumber(totalPriceAmount) }} + + 实际订单总金额: + ¥{{ formatNumber(bszxTotalPrice) }} + + + + 排行榜统计金额: + ¥{{ formatNumber(rankingTotalPrice) }} diff --git a/src/views/bszx/dashboard/components/websiteEdit.vue b/src/views/bszx/dashboard/components/websiteEdit.vue new file mode 100644 index 0000000..d68758f --- /dev/null +++ b/src/views/bszx/dashboard/components/websiteEdit.vue @@ -0,0 +1,413 @@ + + + + diff --git a/src/views/bszx/dashboard/index.vue b/src/views/bszx/dashboard/index.vue new file mode 100644 index 0000000..0da5ca4 --- /dev/null +++ b/src/views/bszx/dashboard/index.vue @@ -0,0 +1,264 @@ + + + + +