Jelajahi Sumber

feat: 新增消费统计和汇总统计接口+前端卡片展示

alphah 1 Minggu lalu
induk
melakukan
24d85e357f

+ 90 - 0
backend/app/plugin/module_payment/account/controller.py

@@ -103,6 +103,96 @@ async def stat_transfer_amount_controller(
     }, msg="统计转账金额成功")
 
 
+@AccountRouter.get(
+    "/stat/consume/amount",
+    summary="统计消费金额",
+    description="统计企业在指定时间范围内的消费总金额",
+    response_model=ResponseSchema[Dict],
+)
+async def stat_consume_amount_controller(
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:account:transfer"]))],
+    tenant_id: Annotated[Optional[int], Query(description="租户ID")] = None,
+    enterprise_id: Annotated[Optional[str], Query(description="企业ID")] = None,
+) -> JSONResponse:
+    """统计消费金额"""
+    from datetime import datetime, timedelta, date
+
+    now = datetime.now()
+    today = date.today()
+
+    # 今天
+    start_of_today = datetime.combine(today, datetime.min.time())
+    end_of_today = start_of_today + timedelta(days=1)
+    amount_of_today = await AccountService.stat_consume_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=start_of_today, end_date=end_of_today,
+    )
+
+    # 近7天
+    start_of_7days_ago = datetime.combine(today - timedelta(days=7), datetime.min.time())
+    amount_of_7days = await AccountService.stat_consume_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=start_of_7days_ago, end_date=now,
+    )
+
+    # 全部
+    amount_of_all = await AccountService.stat_consume_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=None, end_date=None,
+    )
+
+    return SuccessResponse(data={
+        "amount_of_today": amount_of_today,
+        "amount_of_7days": amount_of_7days,
+        "amount_of_all": amount_of_all,
+    }, msg="统计消费金额成功")
+
+
+@AccountRouter.get(
+    "/stat/summary/amount",
+    summary="汇总统计金额",
+    description="统计企业在指定时间范围内的汇总金额(消费+转账)",
+    response_model=ResponseSchema[Dict],
+)
+async def stat_summary_amount_controller(
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:account:transfer"]))],
+    tenant_id: Annotated[Optional[int], Query(description="租户ID")] = None,
+    enterprise_id: Annotated[Optional[str], Query(description="企业ID")] = None,
+) -> JSONResponse:
+    """汇总统计金额(消费+转账)"""
+    from datetime import datetime, timedelta, date
+
+    now = datetime.now()
+    today = date.today()
+
+    # 今天
+    start_of_today = datetime.combine(today, datetime.min.time())
+    end_of_today = start_of_today + timedelta(days=1)
+    amount_of_today = await AccountService.stat_summary_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=start_of_today, end_date=end_of_today,
+    )
+
+    # 近7天
+    start_of_7days_ago = datetime.combine(today - timedelta(days=7), datetime.min.time())
+    amount_of_7days = await AccountService.stat_summary_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=start_of_7days_ago, end_date=now,
+    )
+
+    # 全部
+    amount_of_all = await AccountService.stat_summary_amount_service(
+        auth=auth, tenant_id=tenant_id, enterprise_id=enterprise_id,
+        start_date=None, end_date=None,
+    )
+
+    return SuccessResponse(data={
+        "amount_of_today": amount_of_today,
+        "amount_of_7days": amount_of_7days,
+        "amount_of_all": amount_of_all,
+    }, msg="统计汇总金额成功")
+
+
 @AccountRouter.post(
     "/authorize/apply",
     summary="申请转账授权签约",

+ 55 - 0
backend/app/plugin/module_payment/account/service.py

@@ -96,6 +96,61 @@ class AccountService:
             end_date=end_date,
         )
 
+    @classmethod
+    async def stat_consume_amount_service(
+        cls,
+        auth: AuthSchema,
+        tenant_id: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        start_date: Optional[datetime] = None,
+        end_date: Optional[datetime] = None,
+    ) -> Decimal:
+        """
+        统计消费金额(✅)
+
+        统计企业在指定时间范围内的消费总金额。
+        数据来源: pay_bill 表,consume_type=CONSUME, status=PROCESSED
+        """
+        from app.plugin.module_payment.notification.crud import BillCRUD
+
+        crud = BillCRUD(auth)
+        return await crud.get_consume_amount(
+            tenant_id=tenant_id,
+            enterprise_id=enterprise_id,
+            start_date=start_date,
+            end_date=end_date,
+        )
+
+    @classmethod
+    async def stat_summary_amount_service(
+        cls,
+        auth: AuthSchema,
+        tenant_id: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        start_date: Optional[datetime] = None,
+        end_date: Optional[datetime] = None,
+    ) -> Decimal:
+        """
+        汇总统计金额(✅)
+
+        汇总 = 消费统计 + 转账统计,对应时间段结果相加
+        """
+        transfer_amount = await cls.stat_transfer_amount_service(
+            auth=auth,
+            tenant_id=tenant_id,
+            enterprise_id=enterprise_id,
+            start_date=start_date,
+            end_date=end_date,
+        )
+        consume_amount = await cls.stat_consume_amount_service(
+            auth=auth,
+            tenant_id=tenant_id,
+            enterprise_id=enterprise_id,
+            start_date=start_date,
+            end_date=end_date,
+        )
+        return transfer_amount + consume_amount
+
     @classmethod
     async def authorize_apply_service(
         cls,

+ 38 - 1
backend/app/plugin/module_payment/notification/crud.py

@@ -1,5 +1,10 @@
 from collections.abc import Sequence
-from typing import Any, List
+from typing import Any, List, Optional
+from datetime import datetime
+from decimal import Decimal
+
+from sqlalchemy import func, select
+from sqlalchemy.engine import Result
 
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.base_crud import CRUDBase
@@ -79,6 +84,38 @@ class BillCRUD(CRUDBase[PayBillModel, Any, Any]):
 
         return obj
 
+    async def get_consume_amount(
+        self,
+        enterprise_id: Optional[str] = None,
+        start_date: Optional[datetime] = None,
+        end_date: Optional[datetime] = None,
+        tenant_id: Optional[int] = None,
+    ) -> Decimal:
+        """统计消费金额:consume_type=CONSUME, status=PROCESSED,SUM consume_amount"""
+        conditions = [
+            PayBillModel.consume_type == "CONSUME",
+            PayBillModel.status == "PROCESSED",
+        ]
+
+        if tenant_id:
+            conditions.append(PayBillModel.tenant_id == tenant_id)
+        if enterprise_id:
+            conditions.append(PayBillModel.enterprise_id == enterprise_id)
+        if start_date:
+            conditions.append(PayBillModel.gmt_biz_create >= start_date)
+        if end_date:
+            conditions.append(PayBillModel.gmt_biz_create <= end_date)
+
+        try:
+            sql = select(func.sum(PayBillModel.consume_amount).label("total_amount")).where(
+                *conditions
+            )
+            sql = await self.filter_permissions(sql)
+            result: Result = await self.auth.db.execute(sql)
+            return result.scalars().first() or Decimal(0)
+        except Exception as e:
+            raise CustomException(msg=f"列表查询失败: {e!s}")
+
     async def create_or_update(
         self, pay_no: str, data: dict
     ) -> PayBillModel:

+ 20 - 0
frontend/src/api/module_payment/account.ts

@@ -13,6 +13,26 @@ export const AccountAPI = {
     });
   },
 
+  statConsumeAmount(tenantId?: number, enterpriseId?: string) {
+    return request<
+      ApiResponse<{ amount_of_today: string; amount_of_7days: string; amount_of_all: string }>
+    >({
+      url: `${API_PATH}/stat/consume/amount`,
+      method: "get",
+      params: { tenant_id: tenantId, enterprise_id: enterpriseId },
+    });
+  },
+
+  statSummaryAmount(tenantId?: number, enterpriseId?: string) {
+    return request<
+      ApiResponse<{ amount_of_today: string; amount_of_7days: string; amount_of_all: string }>
+    >({
+      url: `${API_PATH}/stat/summary/amount`,
+      method: "get",
+      params: { tenant_id: tenantId, enterprise_id: enterpriseId },
+    });
+  },
+
   authorizeApply(data: { enterprise_id: string }) {
     return request<ApiResponse<{ sign_url: string }>>({
       url: `${API_PATH}/authorize/apply`,

+ 117 - 9
frontend/src/views/module_payment/account/components/AccountOverview.vue

@@ -201,7 +201,7 @@
         <el-card shadow="hover">
           <div class="stat-item">
             <div class="stat-label">今日转账金额</div>
-            <div class="stat-value balance-value">¥{{ statAmount.amount_of_today || "0.00" }}</div>
+            <div class="stat-value balance-value">¥{{ transferAmount.amount_of_today || "0.00" }}</div>
           </div>
         </el-card>
       </el-col>
@@ -210,7 +210,7 @@
         <el-card shadow="hover">
           <div class="stat-item">
             <div class="stat-label">近7天转账金额</div>
-            <div class="stat-value balance-value">¥{{ statAmount.amount_of_7days || "0.00" }}</div>
+            <div class="stat-value balance-value">¥{{ transferAmount.amount_of_7days || "0.00" }}</div>
           </div>
         </el-card>
       </el-col>
@@ -219,7 +219,65 @@
         <el-card shadow="hover">
           <div class="stat-item">
             <div class="stat-label">累计转账金额</div>
-            <div class="stat-value balance-value">¥{{ statAmount.amount_of_all || "0.00" }}</div>
+            <div class="stat-value balance-value">¥{{ transferAmount.amount_of_all || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="16" class="mt-4">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">今日消费金额</div>
+            <div class="stat-value consume-value">¥{{ consumeAmount.amount_of_today || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">近7天消费金额</div>
+            <div class="stat-value consume-value">¥{{ consumeAmount.amount_of_7days || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">累计消费金额</div>
+            <div class="stat-value consume-value">¥{{ consumeAmount.amount_of_all || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="16" class="mt-4">
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">今日汇总</div>
+            <div class="stat-value summary-value">¥{{ summaryAmount.amount_of_today || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">近7天汇总</div>
+            <div class="stat-value summary-value">¥{{ summaryAmount.amount_of_7days || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover">
+          <div class="stat-item">
+            <div class="stat-label">累计汇总</div>
+            <div class="stat-value summary-value">¥{{ summaryAmount.amount_of_all || "0.00" }}</div>
           </div>
         </el-card>
       </el-col>
@@ -301,7 +359,19 @@ const accountData = ref<{
   scene?: string;
 }>({});
 const recentTransfers = ref<any[]>([]);
-const statAmount = ref<{
+const transferAmount = ref<{
+  amount_of_today?: string;
+  amount_of_7days?: string;
+  amount_of_all?: string;
+}>({});
+
+const consumeAmount = ref<{
+  amount_of_today?: string;
+  amount_of_7days?: string;
+  amount_of_all?: string;
+}>({});
+
+const summaryAmount = ref<{
   amount_of_today?: string;
   amount_of_7days?: string;
   amount_of_all?: string;
@@ -438,14 +508,40 @@ async function fetchAccountInfo() {
   }
 }
 
-async function fetchStatAmount() {
+async function fetchTransferAmount() {
   if (!currentEnterpriseId.value) return;
   loading.value = true;
   try {
     const res = await AccountAPI.statAmount(undefined, currentEnterpriseId.value);
-    statAmount.value = res.data.data || {};
+    transferAmount.value = res.data.data || {};
   } catch (error) {
-    console.error("获取统计金额失败:", error);
+    console.error("获取转账统计金额失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function fetchConsumeAmount() {
+  if (!currentEnterpriseId.value) return;
+  loading.value = true;
+  try {
+    const res = await AccountAPI.statConsumeAmount(undefined, currentEnterpriseId.value);
+    consumeAmount.value = res.data.data || {};
+  } catch (error) {
+    console.error("获取消费统计金额失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function fetchSummaryAmount() {
+  if (!currentEnterpriseId.value) return;
+  loading.value = true;
+  try {
+    const res = await AccountAPI.statSummaryAmount(undefined, currentEnterpriseId.value);
+    summaryAmount.value = res.data.data || {};
+  } catch (error) {
+    console.error("获取汇总统计金额失败:", error);
   } finally {
     loading.value = false;
   }
@@ -453,12 +549,16 @@ async function fetchStatAmount() {
 
 async function handleRefresh() {
   await fetchAccountInfo();
-  await fetchStatAmount();
+  await fetchTransferAmount();
+  await fetchConsumeAmount();
+  await fetchSummaryAmount();
 }
 
 async function refresh() {
   await fetchAccountInfo();
-  await fetchStatAmount();
+  await fetchTransferAmount();
+  await fetchConsumeAmount();
+  await fetchSummaryAmount();
 }
 
 defineExpose({ refresh });
@@ -509,6 +609,14 @@ defineExpose({ refresh });
   .balance-value {
     color: #67c23a;
   }
+
+  .consume-value {
+    color: #409eff;
+  }
+
+  .summary-value {
+    color: #e6a23c;
+  }
 }
 
 .text-truncate {