瀏覽代碼

feat: 统计数据

gatsby 3 周之前
父節點
當前提交
2877bc6d09

+ 8 - 0
backend/app/core/base_crud.py

@@ -524,3 +524,11 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
                 options.append(opt)
 
         return options
+
+
+    async def build_conditions(self, **kwargs) -> builtins.list[ColumnElement]:
+        return await self.__build_conditions(**kwargs) if kwargs else []
+
+
+    async def filter_permissions(self, sql: Select) -> Select:
+        return await self.__filter_permissions(sql)

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

@@ -1,3 +1,4 @@
+from datetime import datetime, timedelta
 import io
 from typing import Annotated, Any, Optional, Dict
 
@@ -39,6 +40,33 @@ AccountRouter = APIRouter(
     tags=["资金专户"],
 )
 
+# 转账金额统计
+@AccountRouter.get(
+    "/stat/transfer/amount",
+    summary="统计转账金额",
+    description="统计企业在指定时间范围内的转账总金额以及每天的转账金额",
+    response_model=ResponseSchema[Dict],
+)
+async def stat_transfer_amount_controller(
+    enterprise_id: Annotated[str, Query(description="企业ID")],
+    # start_date: Annotated[Optional[datetime], Query(description="统计开始日期")],
+    # end_date: Annotated[Optional[datetime], Query(description="统计结束日期")],
+    auth: Annotated[AuthSchema, Depends(AuthPermission(["module_payment:account:transfer"]))],
+) -> JSONResponse:
+    """统计转账金额"""
+    # 统计当天转账金额
+    amount_of_today = await AccountService.stat_transfer_amount_service(auth=auth, enterprise_id=enterprise_id, start_date=datetime.now(), end_date=datetime.now())
+    # 统计近7天转账金额
+    amount_of_7days = await AccountService.stat_transfer_amount_service(auth=auth, enterprise_id=enterprise_id, start_date=datetime.now() - timedelta(days=7), end_date=datetime.now())
+    # 统计全部转账金额
+    amount_of_all = await AccountService.stat_transfer_amount_service(auth=auth, 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",

+ 37 - 0
backend/app/plugin/module_payment/account/crud.py

@@ -1,6 +1,12 @@
+from datetime import datetime
+from decimal import Decimal
+
+from sqlalchemy import func, select
+
 from app.api.v1.module_system.auth.schema import AuthSchema
 from app.core.base_crud import CRUDBase
 from app.core.exceptions import CustomException
+from typing import TYPE_CHECKING, Optional
 
 from .model import AccountModel, TransferModel, DepositModel, WithdrawModel
 from .schema import (
@@ -10,6 +16,9 @@ from .schema import (
     AccountWithdrawSchema,
 )
 
+if TYPE_CHECKING:
+    from sqlalchemy.engine import Result
+
 
 class AccountCRUD(CRUDBase[AccountModel, AccountCreateSchema, AccountCreateSchema]):
     """资金专户 CRUD 操作"""
@@ -87,6 +96,34 @@ class TransferCRUD(CRUDBase[TransferModel, AccountTransferSchema, AccountTransfe
         await self.auth.db.refresh(obj)
 
         return obj
+    
+    # 统计企业在指定时间范围内的转账总金额以及每天的转账金额。
+    async def get_transfer_amount(
+        self, 
+        enterprise_id: str,
+        start_date: Optional[datetime] = None,
+        end_date: Optional[datetime] = None,
+    ) -> Decimal:
+        conditions = [
+            TransferModel.enterprise_id == enterprise_id,
+            TransferModel.status == "SUCCESS",
+        ]
+
+        if start_date:
+            conditions.append(TransferModel.created_time >= start_date)
+        if end_date:
+            conditions.append(TransferModel.created_time <= end_date)
+
+        try:
+            # 统计转时间范围内的转账总金额,字段amount
+            sql = select(func.sum(TransferModel.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}")
 
 
 class DepositCRUD(CRUDBase[DepositModel, AccountDepositSchema, AccountDepositSchema]):

+ 4 - 3
backend/app/plugin/module_payment/account/model.py

@@ -1,4 +1,5 @@
 from datetime import datetime
+from decimal import Decimal
 
 from sqlalchemy import DateTime, String, Text, JSON, Numeric, ForeignKey
 from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -67,7 +68,7 @@ class TransferModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
     account_book_id: Mapped[str | None] = mapped_column(
         String(64), comment="付款方资金专户号"
     )
-    amount: Mapped[float | None] = mapped_column(
+    amount: Mapped[Decimal | None] = mapped_column(
         Numeric(12, 2), comment="转账金额"
     )
     order_title: Mapped[str | None] = mapped_column(
@@ -112,7 +113,7 @@ class DepositModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
     account_book_id: Mapped[str | None] = mapped_column(
         String(64), comment="资金专户号"
     )
-    amount: Mapped[float | None] = mapped_column(
+    amount: Mapped[Decimal | None] = mapped_column(
         Numeric(12, 2), comment="充值金额"
     )
     url: Mapped[str | None] = mapped_column(
@@ -142,7 +143,7 @@ class WithdrawModel(PaymentModelMixin, TenantMixin, EnterpriseMixin):
     account_book_id: Mapped[str | None] = mapped_column(
         String(64), comment="资金专户号"
     )
-    amount: Mapped[float | None] = mapped_column(
+    amount: Mapped[Decimal | None] = mapped_column(
         Numeric(12, 2), comment="提现金额"
     )
     status: Mapped[str] = mapped_column(

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

@@ -39,6 +39,27 @@ from ..openapi.crud import OpenTransferCRUD
 class AccountService:
     """资金专户服务层"""
 
+    @classmethod
+    async def stat_transfer_amount_service(
+        cls,
+        auth: AuthSchema,
+        enterprise_id: str,
+        start_date: Optional[datetime] = None,
+        end_date: Optional[datetime] = None,
+    ) -> Decimal:
+        """
+        统计转账金额(✅)
+
+        统计企业在指定时间范围内的转账总金额以及每天的转账金额。
+        """
+        crud = TransferCRUD(auth)
+        
+        return await crud.get_transfer_amount(
+            enterprise_id=enterprise_id,
+            start_date=start_date,
+            end_date=end_date,
+        )
+
     @classmethod
     async def authorize_apply_service(
         cls,

+ 0 - 109
backend/app/plugin/module_payment/account/tasks.py

@@ -1,109 +0,0 @@
-import asyncio
-import json
-from datetime import datetime
-
-from app.core.logger import log
-from app.core.redis_crud import RedisCURD
-from redis.asyncio import Redis
-
-from .service import AccountService
-from app.api.v1.module_system.auth.schema import AuthSchema
-
-
-class TransferBatchTask:
-    """批量转账后台任务"""
-
-    @classmethod
-    async def process_batch_transfer(cls):
-        """
-        处理批量转账任务
-        从 Redis 队列中取出任务,逐笔处理
-        """
-        redis: Redis = await RedisCURD.get_redis()
-
-        while True:
-            try:
-                # 从队列中取出任务
-                task_data_str = await RedisCURD(redis).rpop("transfer:batch:queue")
-                if not task_data_str:
-                    await asyncio.sleep(1)
-                    continue
-
-                # 解析任务数据
-                task_data = json.loads(task_data_str)
-                batch_id = task_data["batch_id"]
-                tenant_id = task_data["tenant_id"]
-                transfers = task_data["transfers"]
-
-                log.info(f"开始处理批量转账任务: {batch_id}, 总笔数: {len(transfers)}")
-
-                # 更新状态为处理中
-                await RedisCURD(redis).hset(f"transfer:batch:status:{batch_id}", "status", "PROCESSING")
-                await RedisCURD(redis).hset(f"transfer:batch:status:{batch_id}", "updated_at", datetime.now().isoformat())
-
-                # 处理每笔转账
-                for transfer_data in transfers:
-                    out_biz_no = transfer_data.get("out_biz_no")
-                    if not out_biz_no:
-                        log.warning(f"转账记录缺少 out_biz_no: {transfer_data}")
-                        continue
-
-                    try:
-                        # 构建 AuthSchema
-                        auth = AuthSchema(
-                            tenant_id=tenant_id,
-                            user_id="system",
-                            user_name="系统",
-                            enterprise_id=transfer_data.get("enterprise_id", ""),
-                        )
-
-                        # 调用转账服务
-                        from .schema import AccountTransferSchema
-                        transfer_schema = AccountTransferSchema(**transfer_data)
-                        result = await AccountService.transfer_service(auth=auth, data=transfer_schema)
-
-                        # 记录成功结果
-                        await RedisCURD(redis).hset(
-                            f"transfer:batch:result:{batch_id}:{out_biz_no}",
-                            status="SUCCESS",
-                            order_no=result.order_no or "",
-                            fund_order_id=result.fund_order_id or ""
-                        )
-
-                        # 更新成功计数
-                        await RedisCURD(redis).hincrby(f"transfer:batch:status:{batch_id}", "success", 1)
-                        log.info(f"转账成功: {out_biz_no} -> {transfer_data.get('amount')}")
-
-                    except Exception as e:
-                        # 记录失败结果
-                        await RedisCURD(redis).hset(
-                            f"transfer:batch:result:{batch_id}:{out_biz_no}",
-                            status="FAIL",
-                            error_msg=str(e)
-                        )
-
-                        # 更新失败计数
-                        await RedisCURD(redis).hincrby(f"transfer:batch:status:{batch_id}", "failed", 1)
-                        log.error(f"转账失败: {out_biz_no}, 错误: {str(e)}")
-
-                    finally:
-                        # 更新已处理计数
-                        await RedisCURD(redis).hincrby(f"transfer:batch:status:{batch_id}", "processed", 1)
-
-                # 更新状态为完成
-                await RedisCURD(redis).hset(f"transfer:batch:status:{batch_id}", "status", "COMPLETED")
-                await RedisCURD(redis).hset(f"transfer:batch:status:{batch_id}", "updated_at", datetime.now().isoformat())
-
-                log.info(f"批量转账任务处理完成: {batch_id}")
-
-            except Exception as e:
-                log.error(f"处理批量转账任务时出错: {str(e)}")
-                await asyncio.sleep(5)
-
-
-async def start_transfer_batch_task():
-    """
-    启动批量转账后台任务
-    """
-    log.info("批量转账后台任务已启动")
-    await TransferBatchTask.process_batch_transfer()

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

@@ -3,6 +3,16 @@ import request from "@/utils/request";
 const API_PATH = "/payment/account";
 
 export const AccountAPI = {
+  statAmount(enterpriseId: string) {
+    return request<
+      ApiResponse<{ amount_of_today: string; amount_of_7days: string; amount_of_all: string }>
+    >({
+      url: `${API_PATH}/stat/transfer/amount`,
+      method: "get",
+      params: { enterprise_id: enterpriseId },
+    });
+  },
+
   authorizeApply(data: { enterprise_id: string }) {
     return request<ApiResponse<{ sign_url: string }>>({
       url: `${API_PATH}/authorize/apply`,

+ 51 - 2
frontend/src/views/module_payment/account/components/AccountOverview.vue

@@ -186,6 +186,35 @@
       </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 balance-value">¥{{ statAmount.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 balance-value">¥{{ statAmount.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 balance-value">¥{{ statAmount.amount_of_all || "0.00" }}</div>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
     <el-card class="mt-4">
       <template #header>
         <div class="card-header">
@@ -256,6 +285,11 @@ const accountData = ref<{
   scene?: string;
 }>({});
 const recentTransfers = ref<any[]>([]);
+const statAmount = ref<{
+  amount_of_today?: string;
+  amount_of_7days?: string;
+  amount_of_all?: string;
+}>({});
 
 const currentEnterpriseId = computed(
   () => props.enterpriseId || enterpriseStore.getCurrentEnterprise?.enterprise_id
@@ -379,12 +413,27 @@ async function fetchAccountInfo() {
   }
 }
 
-function handleRefresh() {
-  fetchAccountInfo();
+async function fetchStatAmount() {
+  if (!currentEnterpriseId.value) return;
+  loading.value = true;
+  try {
+    const res = await AccountAPI.statAmount(currentEnterpriseId.value);
+    statAmount.value = res.data.data || {};
+  } catch (error) {
+    console.error("获取统计金额失败:", error);
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleRefresh() {
+  await fetchAccountInfo();
+  await fetchStatAmount();
 }
 
 async function refresh() {
   await fetchAccountInfo();
+  await fetchStatAmount();
 }
 
 defineExpose({ refresh });