Pārlūkot izejas kodu

✨ feat(data-overview): 为数据概览模块添加强制刷新功能

- 在路由层为 `/today` 接口添加 `forceRefresh` 查询参数支持
- 在服务层为 `getSummaryStatistics`、`getTodayStatistics` 和 `getUserConsumption` 方法添加 `forceRefresh` 参数,用于跳过缓存直接查询数据库
- 在UI层为概览面板添加强制刷新按钮和状态,优化数据刷新逻辑
- 更新相关Schema定义、类型声明和测试用例以适配新功能

♻️ refactor(data-overview): 重构统计计算逻辑并优化字段映射

- 从 `SummaryStatistics` 接口中移除 `todaySales` 和 `todayOrders` 字段,将其移至独立的今日数据接口
- 将统计查询中的金额字段从 `order.amount` 统一更新为 `order.payAmount`
- 修正支付类型映射逻辑,将微信支付类型从 `1` 更新为 `4`
- 优化测试数据工厂,以反映支付类型的更新
yourname 2 nedēļas atpakaļ
vecāks
revīzija
b7f918baff

+ 6 - 2
packages/data-overview-module-mt/src/routes/today.mt.ts

@@ -3,12 +3,15 @@ import { authMiddleware } from '@d8d/core-module-mt/auth-module-mt/middleware';
 import { AppDataSource, ErrorSchema, parseWithAwait } from '@d8d/shared-utils';
 import { AuthContext } from '@d8d/shared-types';
 import { DataOverviewServiceMt } from '../services';
-import { TodayResponseSchema } from '../schemas';
+import { TodayResponseSchema, TodayQuerySchema } from '../schemas';
 
 const todayRoute = createRoute({
   method: 'get',
   path: '/today',
   middleware: [authMiddleware],
+  request: {
+    query: TodayQuerySchema
+  },
   responses: {
     200: {
       description: '获取今日实时数据成功',
@@ -48,10 +51,11 @@ const todayRoute = createRoute({
 const todayRoutes = new OpenAPIHono<AuthContext>()
   .openapi(todayRoute, async (c) => {
     const user = c.get('user');
+    const queryParams = c.req.valid('query');
 
     try {
       const service = new DataOverviewServiceMt(AppDataSource);
-      const todayStats = await service.getTodayStatistics(user.tenantId);
+      const todayStats = await service.getTodayStatistics(user.tenantId, queryParams.forceRefresh);
 
       const responseData = await parseWithAwait(TodayResponseSchema, {
         data: todayStats,

+ 25 - 8
packages/data-overview-module-mt/src/schemas/index.ts

@@ -21,6 +21,13 @@ export const TimeFilterSchema = z.object({
   ).optional().openapi({
     description: '特定年份统计 (例如: 2024, 2025),提供此参数时将忽略timeRange',
     example: 2025
+  }),
+  forceRefresh: z.preprocess(
+    (val) => val === undefined ? undefined : val === 'true' || val === true,
+    z.boolean()
+  ).optional().openapi({
+    description: '强制刷新,跳过缓存直接读取数据库',
+    example: false
   })
 }).refine((data) => {
   // 如果提供了timeRange为custom,则必须提供startDate和endDate
@@ -67,14 +74,6 @@ export const SummaryStatisticsSchema = z.object({
   creditOrders: z.number().int().openapi({
     description: '额度支付订单数',
     example: 40
-  }),
-  todaySales: z.number().openapi({
-    description: '今日销售额',
-    example: 5000.00
-  }),
-  todayOrders: z.number().int().openapi({
-    description: '今日订单数',
-    example: 10
   })
 });
 
@@ -138,6 +137,13 @@ export const PaginationParamsSchema = z.object({
   sortOrder: z.enum(['asc', 'desc']).optional().default('desc').openapi({
     description: '排序方向',
     example: 'desc'
+  }),
+  forceRefresh: z.preprocess(
+    (val) => val === undefined ? undefined : val === 'true' || val === true,
+    z.boolean()
+  ).optional().openapi({
+    description: '强制刷新,跳过缓存直接读取数据库',
+    example: false
   })
 });
 
@@ -214,5 +220,16 @@ export const UserConsumptionApiResponseSchema = z.object({
   })
 });
 
+// 今日数据查询参数Schema
+export const TodayQuerySchema = z.object({
+  forceRefresh: z.preprocess(
+    (val) => val === undefined ? undefined : val === 'true' || val === true,
+    z.boolean()
+  ).optional().openapi({
+    description: '强制刷新,跳过缓存直接读取数据库',
+    example: false
+  })
+});
+
 // 导出错误Schema
 export { ErrorSchema };

+ 35 - 34
packages/data-overview-module-mt/src/services/data-overview.service.ts

@@ -7,6 +7,7 @@ export interface TimeFilterParams {
   endDate?: string;
   timeRange?: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisYear' | 'lastYear' | 'custom';
   year?: number; // 特定年份,例如2024, 2025
+  forceRefresh?: boolean; // 强制刷新,跳过缓存直接读取数据库
 }
 
 export interface SummaryStatistics {
@@ -16,8 +17,6 @@ export interface SummaryStatistics {
   wechatOrders: number;
   creditSales: number;
   creditOrders: number;
-  todaySales: number;
-  todayOrders: number;
 }
 
 export interface UserConsumptionItem {
@@ -45,6 +44,7 @@ export interface PaginationParams {
   limit?: number;
   sortBy?: 'totalSpent' | 'orderCount' | 'avgOrderAmount' | 'lastOrderDate';
   sortOrder?: 'asc' | 'desc';
+  forceRefresh?: boolean; // 强制刷新,跳过缓存直接读取数据库
 }
 
 export class DataOverviewServiceMt {
@@ -113,16 +113,18 @@ export class DataOverviewServiceMt {
     // 生成缓存键
     const cacheKey = `data_overview:summary:${tenantId}:${params.timeRange || 'today'}:${params.startDate || ''}:${params.endDate || ''}`;
 
-    // 尝试从缓存获取
-    const cached = await this.redisUtil.get(cacheKey);
-    if (cached) {
-      return JSON.parse(cached);
+    // 尝试从缓存获取(除非强制刷新)
+    if (!params.forceRefresh) {
+      const cached = await this.redisUtil.get(cacheKey);
+      if (cached) {
+        return JSON.parse(cached);
+      }
     }
 
     const { startDate, endDate } = this.getDateRange(params);
 
     // 执行统计查询
-    const statistics = await this.calculateStatistics(tenantId, startDate, endDate);
+    const statistics = await this.calculateStatistics(tenantId, startDate, endDate, params.forceRefresh);
 
     // 设置缓存:今日数据5分钟,历史数据30分钟
     const isToday = params.timeRange === 'today' || (!params.timeRange && !params.startDate && !params.endDate);
@@ -135,15 +137,15 @@ export class DataOverviewServiceMt {
   /**
    * 计算统计数据
    */
-  private async calculateStatistics(tenantId: number, startDate: Date, endDate: Date): Promise<SummaryStatistics> {
+  private async calculateStatistics(tenantId: number, startDate: Date, endDate: Date, forceRefresh: boolean = false): Promise<SummaryStatistics> {
     // 使用TypeORM查询构建器进行统计
     const queryBuilder = this.orderRepository.createQueryBuilder('order')
       .select([
         'COUNT(*) as total_orders',
-        'SUM(order.amount) as total_sales',
-        'SUM(CASE WHEN order.payType = :wechatPayType THEN order.amount ELSE 0 END) as wechat_sales',
-        'SUM(CASE WHEN order.payType = :creditPayType THEN order.amount ELSE 0 END) as credit_sales',
-        'COUNT(CASE WHEN order.payType = :wechatPayType THEN 1 END) as wechat_orders',
+        'SUM(order.payAmount) as total_sales',
+        'SUM(CASE WHEN order.payType != :creditPayType THEN order.payAmount ELSE 0 END) as wechat_sales',
+        'SUM(CASE WHEN order.payType = :creditPayType THEN order.payAmount ELSE 0 END) as credit_sales',
+        'COUNT(CASE WHEN order.payType != :creditPayType THEN 1 END) as wechat_orders',
         'COUNT(CASE WHEN order.payType = :creditPayType THEN 1 END) as credit_orders'
       ])
       .where('order.tenantId = :tenantId', { tenantId })
@@ -151,36 +153,33 @@ export class DataOverviewServiceMt {
       .andWhere('order.cancelTime IS NULL') // 排除已取消的订单
       .andWhere('order.createdAt BETWEEN :startDate AND :endDate', { startDate, endDate })
       .setParameters({
-        wechatPayType: 1, // 1=积分支付(假设为微信支付)
-        creditPayType: 3  // 3=额度支付(假设为信用支付)
+        creditPayType: 3  // 3=额度支付
       });
 
     const result = await queryBuilder.getRawOne();
 
-    // 计算今日数据(单独查询以提高性能)
-    const todayStats = await this.getTodayStatistics(tenantId);
-
     return {
       totalSales: Number(result?.total_sales || 0),
       totalOrders: Number(result?.total_orders || 0),
       wechatSales: Number(result?.wechat_sales || 0),
       wechatOrders: Number(result?.wechat_orders || 0),
       creditSales: Number(result?.credit_sales || 0),
-      creditOrders: Number(result?.credit_orders || 0),
-      todaySales: todayStats.todaySales,
-      todayOrders: todayStats.todayOrders
+      creditOrders: Number(result?.credit_orders || 0)
     };
   }
 
   /**
    * 获取今日实时统计数据
    */
-  async getTodayStatistics(tenantId: number): Promise<{ todaySales: number; todayOrders: number }> {
+  async getTodayStatistics(tenantId: number, forceRefresh: boolean = false): Promise<{ todaySales: number; todayOrders: number }> {
     const cacheKey = `data_overview:today:${tenantId}:${new Date().toISOString().split('T')[0]}`;
 
-    const cached = await this.redisUtil.get(cacheKey);
-    if (cached) {
-      return JSON.parse(cached);
+    // 尝试从缓存获取(除非强制刷新)
+    if (!forceRefresh) {
+      const cached = await this.redisUtil.get(cacheKey);
+      if (cached) {
+        return JSON.parse(cached);
+      }
     }
 
     const todayStart = new Date();
@@ -190,7 +189,7 @@ export class DataOverviewServiceMt {
     const queryBuilder = this.orderRepository.createQueryBuilder('order')
       .select([
         'COUNT(*) as today_orders',
-        'SUM(order.amount) as today_sales'
+        'SUM(order.payAmount) as today_sales'
       ])
       .where('order.tenantId = :tenantId', { tenantId })
       .andWhere('order.payState = :payState', { payState: 2 }) // 2=支付成功
@@ -231,10 +230,12 @@ export class DataOverviewServiceMt {
     // 生成缓存键
     const cacheKey = `data_overview:user_consumption:${tenantId}:${params.timeRange || 'all'}:${params.startDate || ''}:${params.endDate || ''}:${paginationParams.page || 1}:${paginationParams.limit || 10}:${paginationParams.sortBy || 'totalSpent'}:${paginationParams.sortOrder || 'desc'}`;
 
-    // 尝试从缓存获取
-    const cached = await this.redisUtil.get(cacheKey);
-    if (cached) {
-      return JSON.parse(cached);
+    // 尝试从缓存获取(除非强制刷新)
+    if (!paginationParams.forceRefresh) {
+      const cached = await this.redisUtil.get(cacheKey);
+      if (cached) {
+        return JSON.parse(cached);
+      }
     }
 
     const { startDate, endDate } = this.getDateRange(params);
@@ -296,9 +297,9 @@ export class DataOverviewServiceMt {
         'order.userId as userId',
         'MAX(user.name) as userName', // 关联用户表获取用户名
         'MAX(user.phone) as userPhone', // 关联用户表获取手机号
-        'SUM(order.amount) as totalSpent',
+        'SUM(order.payAmount) as totalSpent',
         'COUNT(order.id) as orderCount',
-        'AVG(order.amount) as avgOrderAmount',
+        'AVG(order.payAmount) as avgOrderAmount',
         'MAX(order.createdAt) as lastOrderDate'
       ])
       .leftJoin('order.user', 'user') // 关联用户表
@@ -331,11 +332,11 @@ export class DataOverviewServiceMt {
    */
   private getSortField(sortBy: string): string {
     const sortMap: Record<string, string> = {
-      'totalSpent': 'SUM(order.amount)',
+      'totalSpent': 'SUM(order.payAmount)',
       'orderCount': 'COUNT(order.id)',
-      'avgOrderAmount': 'AVG(order.amount)',
+      'avgOrderAmount': 'AVG(order.payAmount)',
       'lastOrderDate': 'MAX(order.createdAt)'
     };
-    return sortMap[sortBy] || 'SUM(order.amount)';
+    return sortMap[sortBy] || 'SUM(order.payAmount)';
   }
 }

+ 2 - 4
packages/data-overview-module-mt/tests/integration/data-overview-routes.integration.test.ts

@@ -131,8 +131,6 @@ describe('多租户数据概览API集成测试', () => {
         expect(typeof data.data.wechatOrders).toBe('number');
         expect(typeof data.data.creditSales).toBe('number');
         expect(typeof data.data.creditOrders).toBe('number');
-        expect(typeof data.data.todaySales).toBe('number');
-        expect(typeof data.data.todayOrders).toBe('number');
       }
     });
 
@@ -322,7 +320,7 @@ describe('多租户数据概览API集成测试', () => {
       // 创建今日订单数据
       await DataOverviewTestDataFactory.createTodayTestOrders(dataSource, 103, 3);
 
-      const response = await client.today.$get({}, {
+      const response = await client.today.$get({ query: {} }, {
         headers: {
           'Authorization': `Bearer ${tenant103Token}`
         }
@@ -345,7 +343,7 @@ describe('多租户数据概览API集成测试', () => {
       const tenant104User = await DataOverviewTestDataFactory.createTestUser(dataSource, 104);
       const tenant104Token = DataOverviewTestDataFactory.generateUserToken(tenant104User);
 
-      const response = await client.today.$get({}, {
+      const response = await client.today.$get({ query: {} }, {
         headers: {
           'Authorization': `Bearer ${tenant104Token}`
         }

+ 1 - 5
packages/data-overview-module-mt/tests/unit/data-overview.service.test.ts

@@ -156,9 +156,7 @@ describe('DataOverviewServiceMt', () => {
         wechatSales: 6000,
         wechatOrders: 30,
         creditSales: 4000,
-        creditOrders: 20,
-        todaySales: 500,
-        todayOrders: 5
+        creditOrders: 20
       };
 
       mockRedisUtil.get.mockResolvedValue(JSON.stringify(cachedStats));
@@ -215,8 +213,6 @@ describe('DataOverviewServiceMt', () => {
       expect(result.creditSales).toBe(4000.50);
       expect(result.wechatOrders).toBe(30);
       expect(result.creditOrders).toBe(20);
-      expect(result.todaySales).toBe(500);
-      expect(result.todayOrders).toBe(5);
 
       expect(mockRedisUtil.set).toHaveBeenCalledWith(
         cacheKey,

+ 4 - 4
packages/data-overview-module-mt/tests/utils/test-data-factory.ts

@@ -169,7 +169,7 @@ export class DataOverviewTestDataFactory {
       costAmount: 80.00,
       payAmount: 100.00,
       orderType: 1,
-      payType: 1, // 1=积分支付(假设为微信支付)
+      payType: 4, // 4=微信支付
       payState: 2, // 2=支付成功
       state: 1,
       addressId,
@@ -198,7 +198,7 @@ export class DataOverviewTestDataFactory {
     count: number,
     options: {
       userId?: number;
-      payType?: 1 | 3; // 1=积分支付(微信支付),3=额度支付(信用支付)
+      payType?: 4 | 3; // 4=微信支付,3=额度支付
       dateOffsetDays?: number; // 日期偏移(负数表示过去)
     } = {}
   ): Promise<OrderMt[]> {
@@ -212,8 +212,8 @@ export class DataOverviewTestDataFactory {
     }
 
     for (let i = 0; i < count; i++) {
-      // 交替创建积分支付和额度支付的订单
-      const payType = options.payType || (i % 2 === 0 ? 1 : 3); // 1=积分支付,3=额度支付
+      // 交替创建微信支付和额度支付的订单
+      const payType = options.payType || (i % 2 === 0 ? 4 : 3); // 4=微信支付,3=额度支付
       const amount = 50.00 + (i * 25.00); // 不同金额
 
       // 处理日期偏移

+ 21 - 15
packages/data-overview-ui-mt/src/components/DataOverviewPanel.tsx

@@ -104,6 +104,7 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
   const [timeFilter, setTimeFilter] = useState<TimeFilter>(defaultTimeFilter);
   const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(PaymentMethod.ALL);
   const [activeTab, setActiveTab] = useState<'overview' | 'user-consumption'>('overview');
+  const [forceRefresh, setForceRefresh] = useState(false);
 
   // 检查权限
   useEffect(() => {
@@ -120,11 +121,11 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
     error: summaryError,
     refetch: refetchSummary
   } = useQuery({
-    queryKey: ['data-overview-summary', timeFilter, tenantId],
+    queryKey: ['data-overview-summary', timeFilter, tenantId, forceRefresh],
     queryFn: async () => {
       try {
         const res = await dataOverviewClientManager.get().summary.$get({
-          query: timeFilter
+          query: forceRefresh ? { ...timeFilter, forceRefresh: true } : timeFilter
         });
 
         if (res.status !== 200) {
@@ -149,10 +150,12 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
     error: todayError,
     refetch: refetchToday
   } = useQuery({
-    queryKey: ['data-overview-today', tenantId],
+    queryKey: ['data-overview-today', tenantId, forceRefresh],
     queryFn: async () => {
       try {
-        const res = await dataOverviewClientManager.get().today.$get();
+        const res = await dataOverviewClientManager.get().today.$get({
+          query: forceRefresh ? { forceRefresh: true } : {}
+        });
 
         if (res.status !== 200) {
           const errorData = await res.json();
@@ -180,9 +183,12 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
 
   // 处理刷新数据
   const handleRefresh = useCallback(() => {
+    setForceRefresh(true);
     refetchSummary();
     refetchToday();
     toast.success('数据已刷新');
+    // 重置 forceRefresh 状态
+    setTimeout(() => setForceRefresh(false), 100);
   }, [refetchSummary, refetchToday]);
 
   // 自动刷新
@@ -211,31 +217,31 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
 
   // 获取卡片值
   const getCardValue = (config: StatCardConfig) => {
-    if (!statistics || !todayStatistics) return 0;
-
     switch (config.type) {
       case StatCardType.TOTAL_SALES:
+        if (!statistics) return 0;
         return getFilteredValue(statistics.totalSales, {
           wechat: statistics.wechatSales,
           credit: statistics.creditSales
         });
       case StatCardType.TOTAL_ORDERS:
+        if (!statistics) return 0;
         return getFilteredValue(statistics.totalOrders, {
           wechat: statistics.wechatOrders,
           credit: statistics.creditOrders
         });
       case StatCardType.TODAY_SALES:
-        return todayStatistics.todaySales;
+        return todayStatistics?.todaySales ?? 0;
       case StatCardType.TODAY_ORDERS:
-        return todayStatistics.todayOrders;
+        return todayStatistics?.todayOrders ?? 0;
       case StatCardType.WECHAT_SALES:
-        return statistics.wechatSales;
+        return statistics?.wechatSales ?? 0;
       case StatCardType.WECHAT_ORDERS:
-        return statistics.wechatOrders;
+        return statistics?.wechatOrders ?? 0;
       case StatCardType.CREDIT_SALES:
-        return statistics.creditSales;
+        return statistics?.creditSales ?? 0;
       case StatCardType.CREDIT_ORDERS:
-        return statistics.creditOrders;
+        return statistics?.creditOrders ?? 0;
       default:
         return 0;
     }
@@ -395,7 +401,7 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
           )}
 
           {/* 统计摘要 */}
-          {!isLoading && !hasError && statistics && (
+          {!isLoading && !hasError && statistics && todayStatistics && (
             <Card>
               <CardHeader>
                 <CardTitle>统计摘要</CardTitle>
@@ -431,13 +437,13 @@ export const DataOverviewPanel: React.FC<DataOverviewPanelProps> = ({
                   <div>
                     <div className="text-muted-foreground">今日销售额</div>
                     <div className="text-2xl font-bold">
-                      ¥{statistics.todaySales.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
+                      ¥{todayStatistics.todaySales.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
                     </div>
                   </div>
                   <div>
                     <div className="text-muted-foreground">今日订单数</div>
                     <div className="text-2xl font-bold">
-                      {statistics.todayOrders.toLocaleString('zh-CN')}
+                      {todayStatistics.todayOrders.toLocaleString('zh-CN')}
                     </div>
                   </div>
                 </div>

+ 2 - 2
packages/data-overview-ui-mt/src/types/dataOverview.ts

@@ -4,6 +4,7 @@ export type TimeFilter = {
   endDate?: string;
   timeRange?: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisYear' | 'lastYear' | 'custom';
   year?: number; // 特定年份,例如2024, 2025
+  forceRefresh?: boolean; // 强制刷新,跳过缓存直接读取数据库
 };
 
 // 数据概览统计类型
@@ -14,8 +15,6 @@ export type SummaryStatistics = {
   wechatOrders: number;
   creditSales: number;
   creditOrders: number;
-  todaySales: number;
-  todayOrders: number;
 };
 
 // 今日数据统计类型
@@ -172,4 +171,5 @@ export interface PaginationParams {
   limit?: number;
   sortBy?: 'totalSpent' | 'orderCount' | 'avgOrderAmount' | 'lastOrderDate';
   sortOrder?: 'asc' | 'desc';
+  forceRefresh?: boolean; // 强制刷新,跳过缓存直接读取数据库
 }