feat: 修复 H5 底部导航覆盖 + 更新项目进度文档

## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
This commit is contained in:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+127
View File
@@ -0,0 +1,127 @@
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, and_
from app.models.preference import MarketingEffect
import hashlib
import logging
logger = logging.getLogger(__name__)
class MarketingEffectService:
def __init__(self, db: AsyncSession):
self.db = db
async def track_event(
self,
user_id: str,
content: str,
product_id: Optional[str] = None,
product_name: Optional[str] = None,
channel: str = "copy",
event_type: str = "copy",
target_audience: str = "",
metadata: Optional[Dict] = None,
) -> Dict[str, Any]:
content_hash = hashlib.sha256(content.encode()).hexdigest()
event = MarketingEffect(
user_id=user_id,
content_hash=content_hash,
product_id=product_id,
product_name=product_name,
channel=channel,
event_type=event_type,
target_audience=target_audience,
metadata=metadata or {},
)
self.db.add(event)
await self.db.flush()
return {
"id": str(event.id),
"event_type": event_type,
"content_hash": content_hash,
}
async def get_effects(
self, user_id: str, page: int = 1, size: int = 20
) -> Dict[str, Any]:
query = (
select(MarketingEffect)
.where(MarketingEffect.user_id == user_id)
.order_by(MarketingEffect.created_at.desc())
.offset((page - 1) * size)
.limit(size)
)
count_query = select(func.count(MarketingEffect.id)).where(
MarketingEffect.user_id == user_id
)
total = await self.db.execute(count_query)
result = await self.db.execute(query)
events = result.scalars().all()
return {
"items": [
{
"id": str(e.id),
"product_name": e.product_name,
"channel": e.channel,
"event_type": e.event_type,
"target_audience": e.target_audience,
"created_at": e.created_at.isoformat() if e.created_at else None,
}
for e in events
],
"total": total.scalar() or 0,
"page": page,
"size": size,
}
async def get_stats(self, user_id: str) -> Dict[str, Any]:
today = datetime.utcnow().date()
week_ago = today - timedelta(days=7)
total_query = select(func.count(MarketingEffect.id)).where(
MarketingEffect.user_id == user_id
)
today_query = select(func.count(MarketingEffect.id)).where(
and_(
MarketingEffect.user_id == user_id,
func.date(MarketingEffect.created_at) == today,
)
)
week_query = select(func.count(MarketingEffect.id)).where(
and_(
MarketingEffect.user_id == user_id,
func.date(MarketingEffect.created_at) >= week_ago,
)
)
copy_query = select(func.count(MarketingEffect.id)).where(
and_(
MarketingEffect.user_id == user_id,
MarketingEffect.event_type == "copy",
)
)
send_query = select(func.count(MarketingEffect.id)).where(
and_(
MarketingEffect.user_id == user_id,
MarketingEffect.event_type == "send",
)
)
totals = await self.db.execute(total_query)
todays = await self.db.execute(today_query)
weeks = await self.db.execute(week_query)
copies = await self.db.execute(copy_query)
sends = await self.db.execute(send_query)
return {
"total_events": totals.scalar() or 0,
"today": todays.scalar() or 0,
"this_week": weeks.scalar() or 0,
"copy_count": copies.scalar() or 0,
"send_count": sends.scalar() or 0,
}