文件预览

service.py

查看 1688 Item Select 技能包中的文件内容。

文件内容

scripts/capabilities/score_and_select/service.py

#!/usr/bin/env python
"""商品评分与圈选服务

内部获取商品明细并完成评分,只输出 Top-N 精简结果。
五大评分维度:销售贡献度(30%) + 流量效率(25%) + 成长潜力(20%) + 营销ROI(15%) + 商品健康度(10%)
"""

from typing import Dict, List, Any

from _http import api_post
from _errors import ServiceError


def fetch_item_detail(strategy: str, limit: int) -> dict:
    """调用远程接口获取商品明细数据。"""
    return api_post(
        "/api/skill_1688_item_select_get_item_detail/1.0.0",
        {"strategy": strategy, "limit": limit},
    )


# ---------------------------------------------------------------------------
# 五维评分
# ---------------------------------------------------------------------------

def _calculate_sales_score(item: Dict, shop_total: Dict) -> float:
    """销售贡献度得分(满分100)"""
    score = 0.0

    if isinstance(shop_total, list):
        shop_total = shop_total[0]

    item_amt = item.get('pay_ord_amt_1d', 0) or 0
    shop_amt = shop_total.get('pay_ord_amt_1d_001', 1) or 1
    amt_ratio = item_amt / shop_amt if shop_amt > 0 else 0

    if amt_ratio >= 0.10:
        amt_score = 100
    elif amt_ratio >= 0.05:
        amt_score = 80
    elif amt_ratio >= 0.02:
        amt_score = 60
    else:
        amt_score = min(amt_ratio * 3000, 60)
    score += amt_score * 0.6

    item_buyer = item.get('pay_ord_byr_cnt_1d', 0) or 0
    shop_buyer = shop_total.get('pay_ord_byr_cnt_1d_001', 1) or 1
    buyer_ratio = item_buyer / shop_buyer if shop_buyer > 0 else 0

    if buyer_ratio >= 0.10:
        buyer_score = 100
    elif buyer_ratio >= 0.05:
        buyer_score = 80
    elif buyer_ratio >= 0.02:
        buyer_score = 60
    else:
        buyer_score = min(buyer_ratio * 3000, 60)
    score += buyer_score * 0.3

    new_buyer_amt = item.get('pay_ord_amt_1d_931', 0) or 0
    new_ratio = new_buyer_amt / item_amt if item_amt > 0 else 0
    new_score = min(new_ratio * 200, 100)
    score += new_score * 0.1

    return round(score, 2)


def _calculate_traffic_score(item: Dict) -> float:
    """流量效率得分(满分100)"""
    score = 0.0

    buyer_cnt = item.get('pay_ord_byr_cnt_1d', 0) or 0
    uv = item.get('ipv_uv_1d', 1) or 1
    cvr = buyer_cnt / uv if uv > 0 else 0

    if cvr >= 0.05:
        cvr_score = 100
    elif cvr >= 0.03:
        cvr_score = 80
    elif cvr >= 0.01:
        cvr_score = 60
    else:
        cvr_score = min(cvr * 2000, 60)
    score += cvr_score * 0.6

    ipv = item.get('ipv_1d', 0) or 0
    imps = item.get('imps_cnt_1d', 1) or 1
    imp_cvr = ipv / imps if imps > 0 else 0
    imp_score = min(imp_cvr * 200, 100)
    score += imp_score * 0.2

    cart_uv = item.get('cart_uv_1d', 0) or 0
    cart_rate = cart_uv / uv if uv > 0 else 0
    cart_score = min(cart_rate * 500, 100)
    score += cart_score * 0.2

    return round(score, 2)


def _calculate_potential_score(item: Dict) -> float:
    """成长潜力得分(满分100)"""
    score = 0.0

    if item.get('is_hqp') == 1:
        score += 30
    if item.get('is_pwp') == 1:
        score += 20
    if item.get('is_sjp') == 1:
        score += 15
    if item.get('is_zdzb') == 1:
        score += 10

    growth_level = item.get('gyp_growth_level', '')
    if '高' in str(growth_level) or 'high' in str(growth_level).lower():
        score += 20
    elif '中' in str(growth_level) or 'medium' in str(growth_level).lower():
        score += 10

    if item.get('is_yx') == 1:
        score += 5

    return min(round(score, 2), 100)


def _calculate_roi_score(item: Dict) -> float:
    """营销ROI得分(满分100)"""
    ad_cost = item.get('ad_cost_1d', 0) or 0
    pay_amt = item.get('pay_ord_amt_1d', 0) or 0

    if ad_cost == 0:
        return 100 if pay_amt > 0 else 50

    roi = pay_amt / ad_cost if ad_cost > 0 else 0

    if roi >= 5:
        return 100
    elif roi >= 3:
        return 80
    elif roi >= 1:
        return 60
    else:
        return 30


def _calculate_health_score(item: Dict) -> float:
    """商品健康度得分(满分100)"""
    score = 0.0

    if item.get('is_no_reason_to_return_7d') == 1:
        score += 10
    if item.get('is_48hour_send') == 1:
        score += 10
    if item.get('is_15day_free_refund') == 1:
        score += 5

    stock = item.get('itm_stock', 0) or 0
    if stock > 100:
        score += 10
    elif stock >= 50:
        score += 5

    refund_amt = item.get('suc_rfd_amt_1d', 0) or 0
    pay_amt = item.get('pay_ord_amt_1d', 1) or 1
    refund_rate = refund_amt / pay_amt if pay_amt > 0 else 0

    if refund_rate < 0.05:
        score += 10
    elif refund_rate < 0.10:
        score += 5

    return min(round(score, 2), 100)


# ---------------------------------------------------------------------------
# 综合评分与分层
# ---------------------------------------------------------------------------

def _calculate_total_score(item: Dict, shop_total: Dict) -> Dict[str, Any]:
    """计算商品综合得分,返回各维度得分和总分。"""
    sales_score = _calculate_sales_score(item, shop_total)
    traffic_score = _calculate_traffic_score(item)
    potential_score = _calculate_potential_score(item)
    roi_score = _calculate_roi_score(item)
    health_score = _calculate_health_score(item)

    total_score = (
        sales_score * 0.30 +
        traffic_score * 0.25 +
        potential_score * 0.20 +
        roi_score * 0.15 +
        health_score * 0.10
    )

    return {
        'sales_score': sales_score,
        'traffic_score': traffic_score,
        'potential_score': potential_score,
        'roi_score': roi_score,
        'health_score': health_score,
        'total_score': round(total_score, 2),
    }


def _classify_product(total_score: float) -> Dict[str, str]:
    """根据总分对商品进行分层。"""
    if total_score >= 80:
        return {'level': 'S级', 'name': '重点推广品',
                'strategy': '加大投入,抢占流量,优化详情页,参与营销活动'}
    elif total_score >= 60:
        return {'level': 'A级', 'name': '潜力培育品',
                'strategy': '针对性优化短板,适度增加预算,测试潜力'}
    elif total_score >= 40:
        return {'level': 'B级', 'name': '维持运营品',
                'strategy': '维持现状,定期检查,作为辅助商品'}
    else:
        return {'level': 'C级', 'name': '优化调整品',
                'strategy': '停止广告,诊断问题,考虑优化或下架'}


def _compact_product(product: Dict, rank: int) -> Dict:
    """将评分结果精简为 LLM 需要的最小字段集。"""
    return {
        "rank": rank,
        "item_id": product.get("item_id"),
        "title": product.get("title"),
        "scores": product.get("scores"),
        "classification": product.get("classification"),
        "key_metrics": {
            "pay_ord_amt_1d": product.get("raw_data", {}).get("pay_ord_amt_1d", 0),
            "pay_ord_byr_cnt_1d": product.get("raw_data", {}).get("pay_ord_byr_cnt_1d", 0),
            "ipv_uv_1d": product.get("raw_data", {}).get("ipv_uv_1d", 0),
            "ad_cost_1d": product.get("raw_data", {}).get("ad_cost_1d", 0),
        },
    }


# ---------------------------------------------------------------------------
# 主入口
# ---------------------------------------------------------------------------

def score_and_select(shop_total: dict, strategy: str = "comprehensive",
                     limit: int = 100, top_n: int = 10) -> dict:
    """商品评分与圈选。

    Args:
        shop_total: 店铺维度数据(作为评分基准)
        strategy:   查询策略 comprehensive / sales / all
        limit:      获取商品数量上限
        top_n:      输出排名前N的商品

    Returns:
        包含 total_scored / returned_count / products / summary 的字典
    """
    # 1. 获取商品明细
    item_data = fetch_item_detail(strategy, limit)

    # 兼容不同返回格式
    if isinstance(item_data, list):
        items = item_data
    elif isinstance(item_data, dict):
        items = item_data.get("products") or item_data.get("items") or item_data.get("data", [])
    else:
        items = []

    if not items:
        raise ServiceError("未获取到商品数据")

    # 2. 评分
    scored = []
    for item in items:
        scores = _calculate_total_score(item, shop_total)
        classification = _classify_product(scores['total_score'])
        scored.append({
            'item_id': item.get('item_id'),
            'title': item.get('title', '未知商品'),
            'scores': scores,
            'classification': classification,
            'raw_data': {
                'pay_ord_amt_1d': item.get('pay_ord_amt_1d', 0),
                'pay_ord_byr_cnt_1d': item.get('pay_ord_byr_cnt_1d', 0),
                'ipv_uv_1d': item.get('ipv_uv_1d', 0),
                'imps_cnt_1d': item.get('imps_cnt_1d', 0),
                'ad_cost_1d': item.get('ad_cost_1d', 0),
                'itm_stock': item.get('itm_stock', 0),
            },
        })
    scored.sort(key=lambda x: x['scores']['total_score'], reverse=True)

    # 3. 取 Top-N 精简输出
    actual_top_n = min(top_n, len(scored))
    top_products = [_compact_product(p, i + 1) for i, p in enumerate(scored[:actual_top_n])]

    return {
        "total_scored": len(scored),
        "returned_count": actual_top_n,
        "products": top_products,
        "summary": {
            "S级": len([r for r in scored if r["classification"]["level"] == "S级"]),
            "A级": len([r for r in scored if r["classification"]["level"] == "A级"]),
            "B级": len([r for r in scored if r["classification"]["level"] == "B级"]),
            "C级": len([r for r in scored if r["classification"]["level"] == "C级"]),
        },
    }