Spaces:
Sleeping
Sleeping
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from sqlalchemy.orm import Session | |
| from typing import List, Optional | |
| import json | |
| from database import get_db | |
| from models.member_level import MemberLevel | |
| from models.user import User | |
| from models.membership import Membership | |
| from models.product import Product | |
| from models.product_media import ProductMedia | |
| from models.media import Media | |
| from schemas.member_level import MemberLevelResponse, MemberLevelCreate, MemberLevelUpdate, PublicMemberLevelResponse | |
| from schemas.operation_log import OperationLogCreate | |
| from routers.auth import get_current_user, get_current_superadmin | |
| from routers.operation_log import create_operation_log | |
| from sqlalchemy import func | |
| from config.member_levels import DEFAULT_MEMBER_LEVELS | |
| router = APIRouter() | |
| # 单位转换常量 | |
| BYTES_PER_KB = 1024 | |
| BYTES_PER_MB = 1024 * 1024 | |
| BYTES_PER_GB = 1024 * 1024 * 1024 | |
| # 计算会员等级的权益条目数 | |
| def calculate_benefit_count(level): | |
| """计算一个会员等级的权益条目数量""" | |
| # 数值类型的权益字段 | |
| numeric_benefits = [ | |
| 'deployment_nodes', | |
| 'product_sku_limit', | |
| 'image_storage_limit', | |
| 'sku_image_limit', | |
| 'sku_video_limit' | |
| ] | |
| # 布尔类型的权益字段 | |
| boolean_benefits = [ | |
| 'template_access', | |
| 'geo_seo_access', | |
| 'social_automation_access', | |
| 'custom_development_access' | |
| ] | |
| count = 0 | |
| # 统计数值类型权益 | |
| for field in numeric_benefits: | |
| if level.get(field, 0) > 0: | |
| count += 1 | |
| # 统计布尔类型权益 | |
| for field in boolean_benefits: | |
| if level.get(field, False): | |
| count += 1 | |
| return count | |
| # 获取会员等级列表(公开接口,无需认证) | |
| async def get_public_member_levels( | |
| db: Session = Depends(get_db) | |
| ): | |
| """公开的会员等级列表接口,用于首页展示""" | |
| member_levels = db.query(MemberLevel).order_by(MemberLevel.level).all() | |
| # 如果数据库中没有数据,使用默认数据 | |
| if not member_levels: | |
| member_levels = DEFAULT_MEMBER_LEVELS | |
| else: | |
| # 转换为字典格式 | |
| member_levels = [ | |
| { | |
| "level": level.level, | |
| "name": level.name, | |
| "description": level.description, | |
| "annual_fee": level.annual_fee, | |
| "discount_price": level.discount_price, | |
| "deployment_nodes": level.deployment_nodes, | |
| "email_marketing_limit": level.email_marketing_limit, | |
| "product_sku_limit": level.product_sku_limit, | |
| "sku_image_limit": level.sku_image_limit, | |
| "image_size_limit": level.image_size_limit, | |
| "sku_video_limit": level.sku_video_limit, | |
| "image_storage_limit": level.image_storage_limit, | |
| "template_access": level.template_access, | |
| "geo_seo_access": level.geo_seo_access, | |
| "social_automation_access": level.social_automation_access, | |
| "custom_development_access": level.custom_development_access | |
| } | |
| for level in member_levels | |
| ] | |
| # 计算最大权益条目数(最高等级的权益必定最多,只需计算最后一个) | |
| max_count = calculate_benefit_count(member_levels[-1]) if member_levels else 0 | |
| return PublicMemberLevelResponse( | |
| member_levels=member_levels, | |
| max_benefit_count=max_count | |
| ) | |
| # 获取会员等级列表 | |
| async def get_member_levels( | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| member_levels = db.query(MemberLevel).order_by(MemberLevel.level).all() | |
| return member_levels | |
| # 通过ID获取会员等级详情 | |
| async def get_member_level( | |
| level_id: int, | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| member_level = db.query(MemberLevel).filter(MemberLevel.id == level_id).first() | |
| if member_level is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Member level with ID '{level_id}' not found" | |
| ) | |
| return member_level | |
| # 创建会员等级(仅超级管理员可操作) | |
| async def create_member_level( | |
| member_level: MemberLevelCreate, | |
| current_user: User = Depends(get_current_superadmin), | |
| db: Session = Depends(get_db) | |
| ): | |
| # 检查等级是否已存在 | |
| existing_level = db.query(MemberLevel).filter(MemberLevel.level == member_level.level).first() | |
| if existing_level: | |
| raise HTTPException( | |
| status_code=status.HTTP_400_BAD_REQUEST, | |
| detail=f"Member level with level '{member_level.level}' already exists" | |
| ) | |
| # 直接使用模型数据创建会员等级 | |
| level_data = member_level.model_dump() | |
| db_level = MemberLevel(**level_data) | |
| db.add(db_level) | |
| db.commit() | |
| db.refresh(db_level) | |
| # 记录操作日志 | |
| create_operation_log( | |
| db=db, | |
| user=current_user, | |
| module="member_level", | |
| operation_type="add", | |
| operation_detail=f"新增会员等级:{db_level.name}(等级:{db_level.level})", | |
| company_code=current_user.company_code | |
| ) | |
| return db_level | |
| # 更新会员等级(仅超级管理员可操作) | |
| async def update_member_level( | |
| level_id: int, | |
| member_level_update: MemberLevelUpdate, | |
| current_user: User = Depends(get_current_superadmin), | |
| db: Session = Depends(get_db) | |
| ): | |
| member_level = db.query(MemberLevel).filter(MemberLevel.id == level_id).first() | |
| if member_level is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Member level with ID '{level_id}' not found" | |
| ) | |
| # 记录修改前的数据 | |
| before_data_dict = { | |
| "name": member_level.name, | |
| "description": member_level.description, | |
| "annual_fee": member_level.annual_fee, | |
| "discount_price": member_level.discount_price, | |
| "deployment_nodes": member_level.deployment_nodes, | |
| "email_marketing_limit": member_level.email_marketing_limit, | |
| "product_sku_limit": member_level.product_sku_limit, | |
| "sku_image_limit": member_level.sku_image_limit, | |
| "image_size_limit": member_level.image_size_limit, | |
| "sku_video_limit": member_level.sku_video_limit, | |
| "image_storage_limit": member_level.image_storage_limit, | |
| "template_access": member_level.template_access, | |
| "geo_seo_access": member_level.geo_seo_access, | |
| "social_automation_access": member_level.social_automation_access, | |
| "custom_development_access": member_level.custom_development_access | |
| } | |
| # 更新会员等级信息 | |
| update_data = member_level_update.model_dump(exclude_unset=True) | |
| for field, value in update_data.items(): | |
| setattr(member_level, field, value) | |
| db.commit() | |
| db.refresh(member_level) | |
| # 记录修改后的数据 | |
| after_data_dict = { | |
| "name": member_level.name, | |
| "description": member_level.description, | |
| "annual_fee": member_level.annual_fee, | |
| "discount_price": member_level.discount_price, | |
| "deployment_nodes": member_level.deployment_nodes, | |
| "email_marketing_limit": member_level.email_marketing_limit, | |
| "product_sku_limit": member_level.product_sku_limit, | |
| "sku_image_limit": member_level.sku_image_limit, | |
| "image_size_limit": member_level.image_size_limit, | |
| "sku_video_limit": member_level.sku_video_limit, | |
| "image_storage_limit": member_level.image_storage_limit, | |
| "template_access": member_level.template_access, | |
| "geo_seo_access": member_level.geo_seo_access, | |
| "social_automation_access": member_level.social_automation_access, | |
| "custom_development_access": member_level.custom_development_access | |
| } | |
| # 只记录有修改的字段,使用中文解析 | |
| modified_fields = [] | |
| field_names = { | |
| "name": "等级名称", | |
| "description": "等级描述", | |
| "annual_fee": "会员每年权益服务费", | |
| "discount_price": "当前优惠价", | |
| "deployment_nodes": "独立站部署节点数", | |
| "email_marketing_limit": "邮件营销推送数量", | |
| "product_sku_limit": "产品SKU数", | |
| "sku_image_limit": "每个SKU可加图片数量", | |
| "image_size_limit": "图片大小限制", | |
| "sku_video_limit": "每个SKU可加视频数量", | |
| "image_storage_limit": "图片空间大小", | |
| "template_access": "模板选择权限", | |
| "geo_seo_access": "GEO,SEO关键词营销", | |
| "social_automation_access": "社交平台自动化营销", | |
| "custom_development_access": "定制化需求开发" | |
| } | |
| for field, chinese_name in field_names.items(): | |
| if before_data_dict[field] != after_data_dict[field]: | |
| modified_fields.append(f"{chinese_name}:{before_data_dict[field]} → {after_data_dict[field]}") | |
| # 生成中文描述的修改内容 | |
| if modified_fields: | |
| modification_detail = ";".join(modified_fields) | |
| else: | |
| modification_detail = "无字段修改" | |
| # 记录操作日志 | |
| create_operation_log( | |
| db=db, | |
| user=current_user, | |
| module="member_level", | |
| operation_type="edit", | |
| operation_detail=f"编辑会员等级:{member_level.name}(等级:{member_level.level})", | |
| before_data=modification_detail, | |
| after_data=None, | |
| company_code=current_user.company_code | |
| ) | |
| return member_level | |
| # 删除会员等级(仅超级管理员可操作) | |
| async def delete_member_level( | |
| level_id: int, | |
| current_user: User = Depends(get_current_superadmin), | |
| db: Session = Depends(get_db) | |
| ): | |
| member_level = db.query(MemberLevel).filter(MemberLevel.id == level_id).first() | |
| if member_level is None: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail=f"Member level with ID '{level_id}' not found" | |
| ) | |
| # 记录会员等级名称和等级,用于日志 | |
| level_name = member_level.name | |
| level = member_level.level | |
| db.delete(member_level) | |
| db.commit() | |
| # 记录操作日志 | |
| create_operation_log( | |
| db=db, | |
| user=current_user, | |
| module="member_level", | |
| operation_type="delete", | |
| operation_detail=f"删除会员等级:{level_name}(等级:{level})", | |
| company_code=current_user.company_code | |
| ) | |
| return {"message": f"Member level with ID '{level_id}' deleted successfully"} | |
| # 获取当前用户会员权益使用情况 | |
| async def get_my_member_benefits( | |
| current_user: User = Depends(get_current_user), | |
| db: Session = Depends(get_db) | |
| ): | |
| """获取当前用户的会员权益及使用情况""" | |
| # 获取用户会员等级 | |
| membership = db.query(Membership).filter( | |
| Membership.company_code == current_user.company_code | |
| ).order_by(Membership.end_date.desc()).first() | |
| if not membership: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="未找到会员信息" | |
| ) | |
| # 获取会员等级详情 | |
| member_level = db.query(MemberLevel).filter(MemberLevel.level == membership.level).first() | |
| if not member_level: | |
| raise HTTPException( | |
| status_code=status.HTTP_404_NOT_FOUND, | |
| detail="未找到会员等级配置" | |
| ) | |
| # 统计数据 | |
| # 1. 产品SKU使用数量 | |
| product_count = db.query(Product).filter( | |
| Product.company_code == current_user.company_code | |
| ).count() | |
| # 2. 图片空间使用情况 (Media模型的file_size是字节) | |
| total_image_size_bytes = db.query(func.sum(Media.file_size)).filter( | |
| Media.company_code == current_user.company_code, | |
| Media.media_type == 'image' | |
| ).scalar() or 0 | |
| # 直接转换为GB (字节 -> GB) | |
| total_image_size_gb = round(total_image_size_bytes / BYTES_PER_GB, 2) | |
| # 3. 每个SKU平均图片数量 | |
| product_media_count = db.query( | |
| ProductMedia.product_id, | |
| func.count(ProductMedia.id).label('media_count') | |
| ).filter( | |
| ProductMedia.company_code == current_user.company_code, | |
| ProductMedia.media_type == 'image' | |
| ).group_by(ProductMedia.product_id).all() | |
| avg_images_per_sku = round( | |
| sum([pm.media_count for pm in product_media_count]) / len(product_media_count), | |
| 1 | |
| ) if product_media_count else 0 | |
| # 4. 视频数量统计 | |
| video_count = db.query(ProductMedia).filter( | |
| ProductMedia.company_code == current_user.company_code, | |
| ProductMedia.media_type == 'video' | |
| ).count() | |
| # 计算每个产品的平均视频数 | |
| avg_videos_per_sku = round(video_count / product_count, 1) if product_count > 0 else 0 | |
| # 5. 邮件营销使用数量(这里暂时返回0,需要邮件模块实现后更新) | |
| email_used = 0 | |
| # 计算会员到期剩余天数 | |
| from datetime import datetime | |
| days_remaining = 0 | |
| if membership.end_date: | |
| delta = membership.end_date - datetime.now().date() | |
| days_remaining = max(0, delta.days) | |
| # 计算各权益的使用率指数 (0-100) | |
| def calculate_usage_index(used, total): | |
| if not total or total == 0: | |
| return 0 | |
| return min(round((used / total) * 100, 1), 100) | |
| benefits = { | |
| "level": membership.level, | |
| "level_name": member_level.name, | |
| "start_date": membership.start_date, | |
| "end_date": membership.end_date, | |
| "days_remaining": days_remaining, | |
| "items": [ | |
| { | |
| "name": "独立站部署节点", | |
| "icon": "🌐", | |
| "used": 1, | |
| "total": member_level.deployment_nodes or 0, | |
| "unit": "个", | |
| "usage_index": calculate_usage_index(1, member_level.deployment_nodes or 0), | |
| "description": "独立网站部署节点数量" | |
| }, | |
| { | |
| "name": "产品SKU数量", | |
| "icon": "📦", | |
| "used": product_count, | |
| "total": member_level.product_sku_limit or 0, | |
| "unit": "个", | |
| "usage_index": calculate_usage_index(product_count, member_level.product_sku_limit or 0), | |
| "description": "已使用产品SKU数量" | |
| }, | |
| { | |
| "name": "邮件营销推送", | |
| "icon": "📧", | |
| "used": email_used, | |
| "total": member_level.email_marketing_limit or 0, | |
| "unit": "封", | |
| "usage_index": calculate_usage_index(email_used, member_level.email_marketing_limit or 0), | |
| "description": "邮件营销推送数量" | |
| }, | |
| { | |
| "name": "图片存储空间", | |
| "icon": "🖼️", | |
| "used": total_image_size_gb, | |
| "total": member_level.image_storage_limit or 0, | |
| "unit": "GB", | |
| "usage_index": calculate_usage_index(total_image_size_gb, member_level.image_storage_limit or 0), | |
| "description": "已使用图片存储空间" | |
| }, | |
| { | |
| "name": "每SKU图片", | |
| "icon": "📸", | |
| "used": avg_images_per_sku, | |
| "total": member_level.sku_image_limit or 0, | |
| "unit": "张", | |
| "usage_index": calculate_usage_index(avg_images_per_sku, member_level.sku_image_limit or 0), | |
| "description": "每个SKU平均图片数量" | |
| }, | |
| { | |
| "name": "每SKU视频", | |
| "icon": "🎬", | |
| "used": avg_videos_per_sku, | |
| "total": member_level.sku_video_limit or 0, | |
| "unit": "个", | |
| "usage_index": calculate_usage_index(avg_videos_per_sku, member_level.sku_video_limit or 0), | |
| "description": "每个SKU平均视频数量" | |
| }, | |
| { | |
| "name": "模板选择权限", | |
| "icon": "🎨", | |
| "used": 1 if member_level.template_access else 0, | |
| "total": 1, | |
| "unit": "", | |
| "usage_index": 100 if member_level.template_access else 0, | |
| "description": "是否可访问模板库", | |
| "is_boolean": True | |
| }, | |
| { | |
| "name": "GEO/SEO关键词营销", | |
| "icon": "🔍", | |
| "used": 1 if member_level.geo_seo_access else 0, | |
| "total": 1, | |
| "unit": "", | |
| "usage_index": 100 if member_level.geo_seo_access else 0, | |
| "description": "是否支持SEO/ GEO营销", | |
| "is_boolean": True | |
| }, | |
| { | |
| "name": "社交平台自动化营销", | |
| "icon": "📱", | |
| "used": 1 if member_level.social_automation_access else 0, | |
| "total": 1, | |
| "unit": "", | |
| "usage_index": 100 if member_level.social_automation_access else 0, | |
| "description": "是否支持社交媒体自动化", | |
| "is_boolean": True | |
| }, | |
| { | |
| "name": "定制化需求开发", | |
| "icon": "🛠️", | |
| "used": 1 if member_level.custom_development_access else 0, | |
| "total": 1, | |
| "unit": "", | |
| "usage_index": 100 if member_level.custom_development_access else 0, | |
| "description": "是否支持定制化开发", | |
| "is_boolean": True | |
| } | |
| ], | |
| "summary": { | |
| "total_benefits": 10, | |
| "enabled_benefits": sum([ | |
| 1, # 部署节点 | |
| 1, # SKU | |
| 1, # 邮件 | |
| 1, # 图片存储 | |
| 1, # SKU图片 | |
| 1, # SKU视频 | |
| 1 if member_level.template_access else 0, | |
| 1 if member_level.geo_seo_access else 0, | |
| 1 if member_level.social_automation_access else 0, | |
| 1 if member_level.custom_development_access else 0 | |
| ]), | |
| "overall_usage_index": round(sum([ | |
| calculate_usage_index(1, member_level.deployment_nodes or 0), | |
| calculate_usage_index(product_count, member_level.product_sku_limit or 0), | |
| calculate_usage_index(email_used, member_level.email_marketing_limit or 0), | |
| calculate_usage_index(total_image_size_gb, member_level.image_storage_limit or 0), | |
| calculate_usage_index(avg_images_per_sku, member_level.sku_image_limit or 0), | |
| calculate_usage_index(avg_videos_per_sku, member_level.sku_video_limit or 0), | |
| 100 if member_level.template_access else 0, | |
| 100 if member_level.geo_seo_access else 0, | |
| 100 if member_level.social_automation_access else 0, | |
| 100 if member_level.custom_development_access else 0 | |
| ]) / 10, 1) | |
| } | |
| } | |
| return benefits |