增加定时任务计算订单。
This commit is contained in:
parent
da0b42cd1d
commit
b32e0fdae5
176
app/api/endpoints/statistics.py
Normal file
176
app/api/endpoints/statistics.py
Normal 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
|
||||
@ -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 = "蜂蜜"
|
||||
|
||||
@ -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
52
app/models/statistics.py
Normal 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
|
||||
@ -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_community_order_statistics():
|
||||
"""每日小区订单统计任务
|
||||
|
||||
|
||||
async def daily_statistics_report():
|
||||
"""每日统计报告任务"""
|
||||
logger.info(f"开始生成每日统计报告: {datetime.now()}")
|
||||
每天早上9点执行,统计每个小区昨日的订单量(已完成)、总订单金额和总支付金额
|
||||
"""
|
||||
logger.info(f"开始执行每日小区订单统计任务: {datetime.now()}")
|
||||
|
||||
try:
|
||||
with get_db_context() as db:
|
||||
|
||||
#获取昨日时间
|
||||
# 计算昨天的日期范围
|
||||
yesterday = datetime.now() - timedelta(days=1)
|
||||
yesterday_start = datetime.combine(yesterday, datetime.min.time())
|
||||
yesterday_end = datetime.combine(yesterday, datetime.max.time())
|
||||
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}
|
||||
|
||||
# 查询每个小区昨日的订单统计
|
||||
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
|
||||
})
|
||||
|
||||
# 按订单数量降序排序
|
||||
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:
|
||||
logger.error(f"生成每日统计报告失败: {str(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("已注册所有每日定时任务")
|
||||
BIN
jobs.sqlite
BIN
jobs.sqlite
Binary file not shown.
27
reports/community_orders/community_orders_2025-03-10.json
Normal file
27
reports/community_orders/community_orders_2025-03-10.json
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user