增加定时任务计算订单。

This commit is contained in:
aaron 2025-03-11 08:59:27 +08:00
parent da0b42cd1d
commit b32e0fdae5
7 changed files with 455 additions and 17 deletions

View File

@ -0,0 +1,176 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import datetime, date, timedelta
from app.models.database import get_db
from app.models.statistics import DailyOrderStats, DailyCommunityOrderStats
from pydantic import BaseModel
from app.api.deps import get_current_user
from enum import Enum
router = APIRouter()
class DailyOrderStatsResponse(BaseModel):
stats_date: date
total_order_count: int
total_original_amount: float
total_final_amount: float
total_communities: int
class Config:
from_attributes = True
class DailyCommunityOrderStatsResponse(BaseModel):
stats_date: date
community_id: int
community_name: str
order_count: int
total_original_amount: float
total_final_amount: float
class Config:
from_attributes = True
class OrderFilter(str, Enum):
ALL = "all" # 所有小区
WITH_ORDERS = "with_orders" # 有订单的小区
WITHOUT_ORDERS = "without_orders" # 没有订单的小区
@router.get("/daily-stats", response_model=List[DailyOrderStatsResponse])
async def get_daily_order_stats(
start_date: Optional[date] = None,
end_date: Optional[date] = None,
limit: int = Query(30, ge=1, le=100),
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
获取每日订单统计数据
- **start_date**: 开始日期可选
- **end_date**: 结束日期可选
- **limit**: 返回记录数量限制默认30条
"""
query = db.query(DailyOrderStats)
if start_date:
query = query.filter(DailyOrderStats.stats_date >= start_date)
if end_date:
query = query.filter(DailyOrderStats.stats_date <= end_date)
# 按日期降序排序,最新的在前面
query = query.order_by(DailyOrderStats.stats_date.desc()).limit(limit)
return query.all()
@router.get("/daily-stats/{stats_date}", response_model=DailyOrderStatsResponse)
async def get_daily_order_stats_by_date(
stats_date: date,
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
获取指定日期的订单统计数据
- **stats_date**: 统计日期
"""
stats = db.query(DailyOrderStats).filter(
DailyOrderStats.stats_date == stats_date
).first()
if not stats:
raise HTTPException(status_code=404, detail=f"未找到{stats_date}的统计数据")
return stats
@router.get("/community-stats", response_model=List[DailyCommunityOrderStatsResponse])
async def get_community_order_stats(
stats_date: Optional[date] = None,
community_id: Optional[int] = None,
filter_type: OrderFilter = OrderFilter.ALL,
limit: int = Query(50, ge=1, le=500),
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
获取小区订单统计数据
- **stats_date**: 统计日期可选
- **community_id**: 小区ID可选
- **filter_type**: 过滤类型all: 所有小区, with_orders: 有订单的小区, without_orders: 没有订单的小区
- **limit**: 返回记录数量限制默认50条
"""
query = db.query(DailyCommunityOrderStats)
if stats_date:
query = query.filter(DailyCommunityOrderStats.stats_date == stats_date)
if community_id:
query = query.filter(DailyCommunityOrderStats.community_id == community_id)
# 根据过滤类型筛选
if filter_type == OrderFilter.WITH_ORDERS:
query = query.filter(DailyCommunityOrderStats.order_count > 0)
elif filter_type == OrderFilter.WITHOUT_ORDERS:
query = query.filter(DailyCommunityOrderStats.order_count == 0)
# 如果没有指定日期,则按日期降序排序
if not stats_date:
query = query.order_by(DailyCommunityOrderStats.stats_date.desc())
# 按订单数量降序排序
if filter_type != OrderFilter.WITHOUT_ORDERS:
query = query.order_by(DailyCommunityOrderStats.order_count.desc())
else:
# 对于无订单的小区,按小区名称排序
query = query.order_by(DailyCommunityOrderStats.community_name)
query = query.limit(limit)
return query.all()
@router.get("/community-stats/{community_id}/trend", response_model=List[DailyCommunityOrderStatsResponse])
async def get_community_order_stats_trend(
community_id: int,
days: int = Query(7, ge=1, le=30),
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
获取指定小区的订单统计趋势数据
- **community_id**: 小区ID
- **days**: 天数默认7天
"""
end_date = datetime.now().date()
start_date = end_date - timedelta(days=days)
stats = db.query(DailyCommunityOrderStats).filter(
DailyCommunityOrderStats.community_id == community_id,
DailyCommunityOrderStats.stats_date >= start_date,
DailyCommunityOrderStats.stats_date <= end_date
).order_by(DailyCommunityOrderStats.stats_date.asc()).all()
return stats
@router.get("/community-stats/date/{stats_date}/summary", response_model=List[DailyCommunityOrderStatsResponse])
async def get_community_stats_summary_by_date(
stats_date: date,
top_n: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db),
current_user = Depends(get_current_user)
):
"""
获取指定日期的小区订单统计摘要前N名
- **stats_date**: 统计日期
- **top_n**: 返回前N名小区默认10
"""
stats = db.query(DailyCommunityOrderStats).filter(
DailyCommunityOrderStats.stats_date == stats_date,
DailyCommunityOrderStats.order_count > 0
).order_by(DailyCommunityOrderStats.order_count.desc()).limit(top_n).all()
if not stats:
raise HTTPException(status_code=404, detail=f"未找到{stats_date}的小区统计数据")
return stats

View File

@ -12,6 +12,8 @@ class Settings(BaseSettings):
# 企业微信机器人系统异常通知
URL_WECOMBOT_SYS_EXCEPTION : str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=edcc54c5-c352-42dd-b9be-0cc5b39cc0dc"
# 企业微信机器人每日报告通知
URL_WECOMBOT_DAILY_REPORT : str = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=6869b6e2-57fc-471a-bb62-028014e2b1c8"
# 积分别名
POINT_ALIAS: str = "蜂蜜"

View File

@ -1,6 +1,6 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity, dashboard, wecom, feedback, timeperiod, community_timeperiod, order_additional_fee, ai, community_set, community_set_mapping, community_profit_sharing, partner, health, scheduler
from app.api.endpoints import wechat,user, address, community, station, order, coupon, community_building, upload, merchant, merchant_product, merchant_order, point, config, merchant_category, log, account,merchant_pay_order, message, bank_card, withdraw, mp, point_product, point_product_order, coupon_activity, dashboard, wecom, feedback, timeperiod, community_timeperiod, order_additional_fee, ai, community_set, community_set_mapping, community_profit_sharing, partner, health, scheduler, statistics
from app.models.database import Base, engine
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
@ -104,6 +104,7 @@ app.include_router(config.router, prefix="/api/config", tags=["系统配置"])
app.include_router(log.router, prefix="/api/logs", tags=["系统日志"])
app.include_router(feedback.router, prefix="/api/feedback", tags=["反馈"])
app.include_router(health.router, prefix="/api/health", tags=["系统健康检查"])
app.include_router(statistics.router, prefix="/api/statistics", tags=["统计数据"])
@app.get("/")
async def root():

52
app/models/statistics.py Normal file
View File

@ -0,0 +1,52 @@
from sqlalchemy import Column, Integer, String, Float, DateTime, Date, ForeignKey
from sqlalchemy.sql import func
from app.models.database import Base
from datetime import datetime
class DailyCommunityOrderStats(Base):
"""每日小区订单统计数据表"""
__tablename__ = "daily_community_order_stats"
id = Column(Integer, primary_key=True, autoincrement=True)
# 统计日期
stats_date = Column(Date, nullable=False, index=True)
# 小区信息
community_id = Column(Integer, ForeignKey("communities.id"), nullable=False, index=True)
community_name = Column(String(100), nullable=False)
# 订单统计
order_count = Column(Integer, nullable=False, default=0) # 订单数量
total_original_amount = Column(Float, nullable=False, default=0) # 总订单金额
total_final_amount = Column(Float, nullable=False, default=0) # 总支付金额
# 元数据
create_time = Column(DateTime(timezone=True), server_default=func.now())
update_time = Column(DateTime(timezone=True), onupdate=func.now())
class Config:
from_attributes = True
class DailyOrderStats(Base):
"""每日订单总体统计数据表"""
__tablename__ = "daily_order_stats"
id = Column(Integer, primary_key=True, autoincrement=True)
# 统计日期
stats_date = Column(Date, nullable=False, unique=True, index=True)
# 订单统计
total_order_count = Column(Integer, nullable=False, default=0) # 总订单数量
total_original_amount = Column(Float, nullable=False, default=0) # 总订单金额
total_final_amount = Column(Float, nullable=False, default=0) # 总支付金额
total_communities = Column(Integer, nullable=False, default=0) # 有订单的小区数量
# 元数据
create_time = Column(DateTime(timezone=True), server_default=func.now())
update_time = Column(DateTime(timezone=True), onupdate=func.now())
class Config:
from_attributes = True

View File

@ -6,38 +6,218 @@ from app.core.scheduler import scheduler
from sqlalchemy import text
import json
import os
from app.core.wecombot import WecomBot
from app.core.config import settings
from app.models.statistics import DailyCommunityOrderStats, DailyOrderStats
from sqlalchemy.exc import IntegrityError
from app.models.community import CommunityDB
logger = logging.getLogger(__name__)
async def daily_statistics_report():
"""每日统计报告任务"""
logger.info(f"开始生成每日统计报告: {datetime.now()}")
async def daily_community_order_statistics():
"""每日小区订单统计任务
每天早上9点执行统计每个小区昨日的订单量已完成总订单金额和总支付金额
"""
logger.info(f"开始执行每日小区订单统计任务: {datetime.now()}")
try:
# 计算昨天的日期范围
yesterday = datetime.now() - timedelta(days=1)
yesterday_start = datetime(yesterday.year, yesterday.month, yesterday.day, 0, 0, 0)
yesterday_end = datetime(yesterday.year, yesterday.month, yesterday.day, 23, 59, 59)
yesterday_date = yesterday.date()
yesterday_str = yesterday.strftime("%Y-%m-%d")
with get_db_context() as db:
# 获取所有小区列表
all_communities = db.query(CommunityDB).all()
community_dict = {community.id: community.name for community in all_communities}
#获取昨日时间
yesterday = datetime.now() - timedelta(days=1)
yesterday_start = datetime.combine(yesterday, datetime.min.time())
yesterday_end = datetime.combine(yesterday, datetime.max.time())
# 查询每个小区昨日的订单统计
result = db.execute(
text("""
SELECT
c.id AS community_id,
c.name AS community_name,
COUNT(o.orderid) AS order_count,
COALESCE(SUM(o.original_amount + o.additional_fee_amount), 0) AS total_original_amount,
COALESCE(SUM(o.final_amount), 0) AS total_final_amount
FROM
shipping_orders o
JOIN
communities c ON o.address_community_id = c.id
WHERE
o.create_time BETWEEN :start_time AND :end_time
AND o.status = 'COMPLETED'
GROUP BY
c.id, c.name
ORDER BY
order_count DESC
"""),
{
"start_time": yesterday_start,
"end_time": yesterday_end
}
)
# 转换结果为字典以小区ID为键
community_stats_dict = {}
for row in result:
community_stats_dict[row.community_id] = {
"community_id": row.community_id,
"community_name": row.community_name,
"order_count": row.order_count,
"total_original_amount": float(row.total_original_amount),
"total_final_amount": float(row.total_final_amount)
}
# 确保每个小区都有一条记录
community_stats = []
for community_id, community_name in community_dict.items():
if community_id in community_stats_dict:
# 使用查询结果
community_stats.append(community_stats_dict[community_id])
else:
# 创建零值记录
community_stats.append({
"community_id": community_id,
"community_name": community_name,
"order_count": 0,
"total_original_amount": 0.0,
"total_final_amount": 0.0
})
except Exception as e:
logger.error(f"生成每日统计报告失败: {str(e)}")
# 按订单数量降序排序
community_stats.sort(key=lambda x: x["order_count"], reverse=True)
# 计算总计(只计算有订单的小区)
communities_with_orders = [stat for stat in community_stats if stat["order_count"] > 0]
total_order_count = sum(item["order_count"] for item in communities_with_orders)
total_original_amount = sum(item["total_original_amount"] for item in communities_with_orders)
total_final_amount = sum(item["total_final_amount"] for item in communities_with_orders)
total_communities = len(communities_with_orders)
# 保存到数据库 - 总体统计
try:
# 检查是否已存在该日期的记录
existing_stats = db.query(DailyOrderStats).filter(
DailyOrderStats.stats_date == yesterday_date
).first()
if existing_stats:
# 更新现有记录
existing_stats.total_order_count = total_order_count
existing_stats.total_original_amount = total_original_amount
existing_stats.total_final_amount = total_final_amount
existing_stats.total_communities = total_communities
existing_stats.update_time = datetime.now()
else:
# 创建新记录
daily_stats = DailyOrderStats(
stats_date=yesterday_date,
total_order_count=total_order_count,
total_original_amount=total_original_amount,
total_final_amount=total_final_amount,
total_communities=total_communities
)
db.add(daily_stats)
# 保存到数据库 - 小区统计
# 先删除该日期的所有小区统计记录,然后重新插入
db.query(DailyCommunityOrderStats).filter(
DailyCommunityOrderStats.stats_date == yesterday_date
).delete()
# 批量插入小区统计记录
for stat in community_stats:
community_stat = DailyCommunityOrderStats(
stats_date=yesterday_date,
community_id=stat["community_id"],
community_name=stat["community_name"],
order_count=stat["order_count"],
total_original_amount=stat["total_original_amount"],
total_final_amount=stat["total_final_amount"]
)
db.add(community_stat)
db.commit()
logger.info(f"已将{yesterday_str}的订单统计数据保存到数据库,共{len(community_stats)}个小区")
except IntegrityError as e:
db.rollback()
logger.error(f"保存订单统计数据到数据库失败: {str(e)}")
except Exception as e:
db.rollback()
logger.error(f"保存订单统计数据到数据库失败: {str(e)}")
# 生成报告消息
message = f"""## {yesterday_str} 小区订单统计报告
### 总计
- 订单总数: {total_order_count}
- 订单总金额: ¥{total_original_amount:.2f}
- 支付总金额: ¥{total_final_amount:.2f}
- 有订单小区数: {total_communities}
- 总小区数: {len(community_stats)}
### 小区排名 (前5名)
"""
# 添加前5个小区的数据
for i, item in enumerate(communities_with_orders[:5], 1):
message += f"""
{i}. **{item['community_name']}**
- 订单数: {item['order_count']}
- 订单金额: ¥{item['total_original_amount']:.2f}
- 支付金额: ¥{item['total_final_amount']:.2f}
"""
# 发送企业微信消息
if communities_with_orders and settings.URL_WECOMBOT_DAILY_REPORT:
try:
wecom_bot = WecomBot(settings.URL_WECOMBOT_DAILY_REPORT)
await wecom_bot.send_markdown(message)
logger.info("每日小区订单统计报告已发送到企业微信")
except Exception as e:
logger.error(f"发送企业微信消息失败: {str(e)}")
logger.info("每日小区订单统计任务完成")
except Exception as e:
logger.error(f"每日小区订单统计任务失败: {str(e)}")
def run_daily_community_order_statistics():
"""
包装异步函数的同步函数用于APScheduler调度
"""
try:
# 获取或创建事件循环
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 运行异步任务
if loop.is_running():
# 如果循环已经在运行使用create_task
future = asyncio.ensure_future(daily_community_order_statistics())
# 可以添加回调函数处理结果
future.add_done_callback(lambda f: logger.info("每日小区订单统计任务完成"))
else:
# 如果循环没有运行,直接运行到完成
loop.run_until_complete(daily_community_order_statistics())
except Exception as e:
logger.error(f"运行每日小区订单统计任务失败: {str(e)}")
def register_daily_tasks():
"""注册所有每日定时任务"""
# 每天早上8点生成统计报告
# 每天早上9点执行小区订单统计
scheduler.add_cron_job(
daily_statistics_report,
hour=8,
run_daily_community_order_statistics, # 使用同步包装函数
hour=3,
minute=0,
job_id="daily_stats_report"
job_id="daily_community_order_stats"
)
logger.info("已注册所有每日定时任务")

Binary file not shown.

View File

@ -0,0 +1,27 @@
{
"date": "2025-03-10",
"total_stats": {
"order_count": 2,
"total_original_amount": 11.0,
"total_final_amount": 11.0,
"total_communities": 1,
"all_communities_count": 2
},
"community_stats": [
{
"community_id": 1,
"community_name": "龙光·天悦龙庭",
"order_count": 2,
"total_original_amount": 11.0,
"total_final_amount": 11.0
},
{
"community_id": 2,
"community_name": "龙湖·云麓小区",
"order_count": 0,
"total_original_amount": 0.0,
"total_final_amount": 0.0
}
],
"generated_at": "2025-03-11T08:30:00.766323"
}