mengchang 2 miesięcy temu
rodzic
commit
0ecfec5734

+ 276 - 105
backend/routers/estimate.py

@@ -2,6 +2,7 @@ from fastapi import APIRouter, Request, Depends, Form
 from fastapi.responses import HTMLResponse
 from fastapi.responses import HTMLResponse
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
 from sqlalchemy import text
 from sqlalchemy import text
+from collections import defaultdict
 from database import SessionLocal, redis_client
 from database import SessionLocal, redis_client
 from services.template_service import build_template
 from services.template_service import build_template
 from services.price_service import apply_adjust
 from services.price_service import apply_adjust
@@ -948,113 +949,283 @@ async def simulate_calc(
         }
         }
     )
     )
 
 
-    
-@router.post("/estimate2", response_class=HTMLResponse)
-def estimate_submit(
-    request: Request,
-    machine_id: int = Form(...),
-    capacity: str = Form(...),
-    option_ids: list[int] = Form([]),
-    db: Session = Depends(get_db)
-):
-    # ---------- 基础价格 ----------
-    base_price = db.execute(
-        text("""
-            SELECT base_price
-            FROM machine_base_price
-            WHERE machine_id=:mid AND capacity=:cap
-        """),
-        {"mid": machine_id, "cap": capacity}
-    ).scalar() or 0
-
-    price = float(base_price)
-
-    # ---------- 扣减项 ----------
-    for oid in option_ids:
-        r = db.execute(
-            text("""
-                SELECT factor, absolute_deduct
-                FROM price_option_factor
-                WHERE option_id=:oid
-            """),
-            {"oid": oid}
-        ).fetchone()
-
-        if r:
-            price = price * float(r.factor) - float(r.absolute_deduct)
-
-    # ---------- 调节系数 ----------
-    brand_id = db.execute(
-        text("""
-            SELECT brand_id
-            FROM t_machine
-            WHERE machine_id=:mid
-        """),
-        {"mid": machine_id}
-    ).scalar()
-
-    price = apply_adjust(db, machine_id, brand_id, price)
-    price = round(max(price, 0), 2)
-
-    # ---------- 估价版本 ----------
-    version_no = str(uuid.uuid4())[:8]
-
-    db.execute(
-        text("""
-            INSERT INTO estimate_record
-            (machine_id, capacity, option_ids, final_price, version_no)
-            VALUES (:m, :c, :o, :p, :v)
-        """),
-        {
-            "m": machine_id,
-            "c": capacity,
-            "o": ",".join(map(str, option_ids)),
-            "p": price,
-            "v": version_no
+
+SPECIAL_DISCOUNT_RATIO = 0.70   # 特殊项触发后,对剩余价格打7折
+
+
+REPAIR_LEVEL_CAP = {
+    3: 0.60,   # 核心维修,最多吃掉基础价60%
+    2: 0.35,   # 重要维修
+    1: 0.15,   # 次要维修
+}
+
+
+def calculate_price(db, base_price: float, selected_option_ids: list[int]):
+
+    if not selected_option_ids:
+        return {
+            "base_price": base_price,
+            "final_price": base_price,
+            "details": []
         }
         }
-    )
-    db.commit()
 
 
-    # ---------- 模板(Redis 缓存) ----------
-    cache_key = f"template:{machine_id}"
-    cached = redis_client.get(cache_key) if redis_client else None
+    # -------------------------------------------------
+    # 1. 读取 option 规则
+    # -------------------------------------------------
 
 
-    if cached:
-        template = json.loads(cached)
-    else:
-        rows = db.execute(text("SELECT * FROM release_option")).fetchall()
-        template = build_template(rows)
-        if redis_client:
-            redis_client.set(cache_key, json.dumps(template), ex=3600)
-
-    # ---------- 重新加载页面 ----------
-    machines = db.execute(
-        text("""
-            SELECT machine_id, name
-            FROM t_machine
-            ORDER BY brand_name, name
-        """)
-    ).fetchall()
-
-    capacities = db.execute(
-        text("""
-            SELECT capacity
-            FROM machine_base_price
-            WHERE machine_id=:mid
-        """),
-        {"mid": machine_id}
-    ).fetchall()
+    rows = db.execute(text("""
+        SELECT
+            f.option_id,
+            f.factor,
+            f.absolute_deduct,
+            f.group_code,
+            f.severity_level,
+            f.sub_weight,
+            f.is_special,
+            f.repair_level,
+            o.option_name
+        FROM price_option_factor f
+        LEFT JOIN release_option o
+            ON o.option_id = f.option_id
+        WHERE f.option_id IN :ids
+    """), {
+        "ids": tuple(selected_option_ids)
+    }).mappings().all()
+
+    # -------------------------------------------------
+    # 2. 分组
+    # -------------------------------------------------
+
+    group_map = defaultdict(list)
+    special_items = []
+    repair_items = []
 
 
-    return request.app.state.templates.TemplateResponse(
-        "estimate.html",
-        {
-            "request": request,
-            "machines": machines,
-            "capacities": capacities,
-            "template": template,
-            "price": price,
-            "version": version_no,
-            "selected_machine": machine_id,
-            "selected_capacity": capacity
+    for r in rows:
+        r = dict(r)
+
+        if r["is_special"]:
+            special_items.append(r)
+
+        if r["repair_level"] and r["repair_level"] > 0:
+            repair_items.append(r)
+
+        group_code = r["group_code"] or "default"
+        group_map[group_code].append(r)
+
+    # -------------------------------------------------
+    # 3. 读取分组配置
+    # -------------------------------------------------
+
+    group_rows = db.execute(text("""
+        SELECT
+            group_code,
+            cap_ratio,
+            group_weight
+        FROM price_damage_group
+    """)).mappings().all()
+
+    group_conf = {r["group_code"]: r for r in group_rows}
+
+    # -------------------------------------------------
+    # 4. 读取组联动
+    # -------------------------------------------------
+
+    override_rows = db.execute(text("""
+        SELECT
+            trigger_group_code,
+            target_group_code,
+            override_type,
+            override_value
+        FROM price_group_override
+    """)).mappings().all()
+
+    overrides = defaultdict(list)
+    for r in override_rows:
+        overrides[r["trigger_group_code"]].append(r)
+
+    # -------------------------------------------------
+    # 5. 普通损伤组计算(不含repair组)
+    # -------------------------------------------------
+
+    group_result = {}
+
+    for group_code, items in group_map.items():
+
+        if group_code == "repair":
+            continue
+
+        effective = [
+            i for i in items
+            if (i["factor"] or 0) > 0 or (i["absolute_deduct"] or 0) > 0
+        ]
+
+        if not effective:
+            continue
+
+        effective.sort(
+            key=lambda x: (
+                x["severity_level"] or 0,
+                x["factor"] or 0
+            ),
+            reverse=True
+        )
+
+        main = effective[0]
+        minors = effective[1:]
+
+        main_deduct = base_price * (main["factor"] or 0) + (main["absolute_deduct"] or 0)
+
+        minor_sum = 0
+        for m in minors:
+            w = m["sub_weight"] if m["sub_weight"] is not None else 0.3
+            minor_sum += base_price * (m["factor"] or 0) * w
+
+        raw = main_deduct + minor_sum
+
+        conf = group_conf.get(group_code)
+        if conf:
+            cap = main_deduct * float(conf["cap_ratio"])
+            deduct = min(raw, cap)
+            deduct *= float(conf["group_weight"])
+        else:
+            deduct = raw
+
+        group_result[group_code] = {
+            "deduct": deduct,
+            "main": main,
+            "has_effect": deduct > 0
         }
         }
-    )
+
+    # -------------------------------------------------
+    # 6. repair 组分级模型
+    # -------------------------------------------------
+
+    repair_deduct = 0
+    repair_detail = []
+
+    if repair_items:
+
+        by_level = defaultdict(list)
+        for r in repair_items:
+            by_level[r["repair_level"]].append(r)
+
+        for level, items in by_level.items():
+
+            items = [
+                i for i in items
+                if (i["factor"] or 0) > 0 or (i["absolute_deduct"] or 0) > 0
+            ]
+
+            if not items:
+                continue
+
+            # 同级只取最严重
+            items.sort(key=lambda x: x["factor"] or 0, reverse=True)
+
+            main = items[0]
+
+            d = base_price * (main["factor"] or 0) + (main["absolute_deduct"] or 0)
+
+            cap_ratio = REPAIR_LEVEL_CAP.get(level, 1.0)
+
+            d = min(d, base_price * cap_ratio)
+
+            repair_deduct += d
+
+            repair_detail.append({
+                "repair_level": level,
+                "option_name": main["option_name"],
+                "deduct": round(d, 2)
+            })
+
+        group_result["repair"] = {
+            "deduct": repair_deduct,
+            "main": None,
+            "has_effect": repair_deduct > 0,
+            "detail": repair_detail
+        }
+
+    # -------------------------------------------------
+    # 7. 组联动
+    # -------------------------------------------------
+
+    disabled_groups = set()
+    group_weight_override = {}
+
+    for trigger, rules in overrides.items():
+
+        if trigger not in group_result:
+            continue
+
+        if not group_result[trigger]["has_effect"]:
+            continue
+
+        for rule in rules:
+
+            target = rule["target_group_code"]
+
+            if rule["override_type"] == "skip":
+                disabled_groups.add(target)
+
+            elif rule["override_type"] == "weight":
+                group_weight_override[target] = float(rule["override_value"])
+
+    # -------------------------------------------------
+    # 8. 汇总普通扣减
+    # -------------------------------------------------
+
+    total_deduct = 0
+    details = []
+
+    for group_code, g in group_result.items():
+
+        if group_code in disabled_groups:
+            continue
+
+        d = g["deduct"]
+
+        if group_code in group_weight_override:
+            d *= group_weight_override[group_code]
+
+        total_deduct += d
+
+        if group_code == "repair":
+            details.append({
+                "group": "repair",
+                "detail": g["detail"],
+                "group_deduct": round(d, 2)
+            })
+        else:
+            details.append({
+                "group": group_code,
+                "option_name": g["main"]["option_name"],
+                "group_deduct": round(d, 2)
+            })
+
+    price_after_damage = base_price - total_deduct
+    if price_after_damage < 0:
+        price_after_damage = 0
+
+    # -------------------------------------------------
+    # 9. 特殊项整体折价
+    # -------------------------------------------------
+
+    special_applied = []
+
+    if special_items:
+
+        # 你后面可以在这里按 option 决定不同折扣
+        price_after_damage *= SPECIAL_DISCOUNT_RATIO
+
+        for s in special_items:
+            special_applied.append(s["option_name"])
+
+    final_price = round(price_after_damage, 2)
+
+    return {
+        "base_price": round(base_price, 2),
+        "final_price": final_price,
+        "damage_deduct": round(total_deduct, 2),
+        "special_discount_applied": special_applied,
+        "details": details
+    }

+ 62 - 0
tools/import_damage_override.py

@@ -0,0 +1,62 @@
+import pymysql
+
+conn = pymysql.connect(
+    host="127.0.0.1",
+    user="root",
+    password="root",
+    database="recycle",
+    charset="utf8mb4"
+)
+
+damage_groups = [
+    ("boot",          "开机状态组",   1.00, 1.00),
+    ("body",          "机身外观组",   1.20, 1.00),
+    ("screen",        "屏幕综合组",   1.25, 1.10),
+    ("account",       "账号状态组",   1.00, 1.00),
+    ("function",      "功能异常组",   1.15, 1.00),
+    ("hinge",         "转轴结构组",   1.10, 1.00),
+    ("repair",        "维修履历组",   1.30, 1.10),
+    ("other_special", "其他功能问题组", 1.00, 1.00),
+]
+
+group_overrides = [
+    # 有维修 → 屏幕组权重下降
+    ("repair", "screen", "weight", 0.85),
+
+    # 无法开机类问题存在时,功能异常组跳过
+    ("boot", "function", "skip", None),
+]
+
+
+with conn.cursor() as cur:
+
+    # ----------------------------
+    # price_damage_group
+    # ----------------------------
+    for g in damage_groups:
+        cur.execute("""
+            INSERT INTO price_damage_group
+                (group_code, group_name, cap_ratio, group_weight)
+            VALUES (%s,%s,%s,%s)
+            ON DUPLICATE KEY UPDATE
+                group_name=VALUES(group_name),
+                cap_ratio=VALUES(cap_ratio),
+                group_weight=VALUES(group_weight)
+        """, g)
+
+    # ----------------------------
+    # price_group_override
+    # ----------------------------
+    for o in group_overrides:
+        cur.execute("""
+            INSERT INTO price_group_override
+                (trigger_group_code, target_group_code, override_type, override_value)
+            VALUES (%s,%s,%s,%s)
+            ON DUPLICATE KEY UPDATE
+                override_type=VALUES(override_type),
+                override_value=VALUES(override_value)
+        """, o)
+
+    conn.commit()
+
+print("price_damage_group / price_group_override 初始化完成")

+ 272 - 0
tools/init_price_option_factor.py

@@ -0,0 +1,272 @@
+# import pymysql
+
+# DB = dict(
+#     host="127.0.0.1",
+#     user="root",
+#     password="root",
+#     database="recycle",
+#     charset="utf8mb4"
+# )
+
+# GROUP_MAP = {
+#     "账号情况": "account",
+#     "机身外观": "body",
+#     "屏幕外观": "screen_appearance",
+#     "屏幕显示": "screen_display",
+#     "触摸": "touch",
+#     "拍摄": "camera",
+#     "WiFi/蓝牙": "wifi",
+#     "通话": "call",
+#     "面容/指纹": "biometrics",
+#     "其他功能问题(可多选或不选)": "other_function",
+#     "整机维修(可多选或不选)": "repair",
+#     "转轴情况": "hinge",
+# }
+
+# SPECIAL_OPTION_IDS = {
+#     100560,   # 已开启丢失模式
+# }
+
+# # `repair_level` tinyint DEFAULT '0' COMMENT '维修分级:0非维修 1次要 2重要 3核心',
+# repair_level_map = {
+#     "core": 3,
+#     "important": 2,
+#     "minor": 1
+# }
+# REPAIR_LEVEL_MAP = {
+#     # core
+#     100384: repair_level_map["core"],
+
+#     # important
+#     100346: repair_level_map["important"],
+#     100530: repair_level_map["important"],
+#     100382: repair_level_map["important"],
+#     100383: repair_level_map["important"],
+
+#     # minor
+#     100381: repair_level_map["minor"],
+#     100410: repair_level_map["minor"],
+#     100529: repair_level_map["minor"],
+# }
+
+# def calc_severity(factor: float) -> int:
+#     if factor >= 0.40:
+#         return 3
+#     if factor >= 0.20:
+#         return 2
+#     if factor > 0:
+#         return 1
+#     return 0
+
+
+# def main():
+
+#     conn = pymysql.connect(**DB)
+#     cur = conn.cursor(pymysql.cursors.DictCursor)
+
+#     # 取出所有 option + factor + 维度名称
+#     cur.execute("""
+#         SELECT
+#             f.id,
+#             f.option_id,
+#             f.factor,
+#             o.option_key_name
+#         FROM price_option_factor f
+#         LEFT JOIN release_option o
+#             ON f.option_id = o.option_id
+#     """)
+
+#     rows = cur.fetchall()
+
+#     print("rows:", len(rows))
+
+#     for r in rows:
+
+#         option_id = int(r["option_id"])
+#         factor = float(r["factor"] or 0)
+#         option_key_name = r["option_key_name"]
+
+#         group_code = GROUP_MAP.get(option_key_name, "other")
+#         severity_level = calc_severity(factor)
+
+#         # 次瑕疵折扣系数(统一先给 0.4)
+#         sub_weight = 0.4
+
+#         is_special = 1 if option_id in SPECIAL_OPTION_IDS else 0
+
+#         repair_level = REPAIR_LEVEL_MAP.get(option_id)
+
+#         cur.execute("""
+#             UPDATE price_option_factor
+#             SET
+#                 group_code = %s,
+#                 severity_level = %s,
+#                 sub_weight = %s,
+#                 is_special = %s,
+#                 repair_level = %s
+#             WHERE id = %s
+#         """, (
+#             group_code,
+#             severity_level,
+#             sub_weight,
+#             is_special,
+#             repair_level,
+#             r["id"]
+#         ))
+
+#     conn.commit()
+#     cur.close()
+#     conn.close()
+
+#     print("done.")
+
+
+
+import pymysql
+
+conn = pymysql.connect(
+    host="127.0.0.1",
+    user="root",
+    password="root",
+    database="recycle",
+    charset="utf8mb4",
+    cursorclass=pymysql.cursors.DictCursor
+)
+
+GROUP_MAP = {
+    "boot": ["开机情况"],
+
+    "body": ["机身外观"],
+
+    # 屏幕综合组 = 外观 + 显示 + 触摸
+    "screen": ["屏幕外观", "屏幕显示", "触摸"],
+
+    "account": ["账号情况"],
+
+    "function": ["WiFi/蓝牙", "面容/指纹", "拍摄", "通话"],
+
+    "hinge": ["转轴情况"],
+
+    "repair": ["整机维修(可多选或不选)"],
+
+    "other_special": ["其他功能问题(可多选或不选)"]
+}
+
+repair_level_map = {
+    "core": 3,
+    "important": 2,
+    "minor": 1
+}
+
+
+def get_group_code(row):
+    for g, keys in GROUP_MAP.items():
+        if row["option_key_name"] in keys:
+            return g
+    return None
+
+
+def calc_severity(factor):
+    if factor is None:
+        return 0
+
+    f = float(factor)
+
+    if f <= 0:
+        return 0
+    elif f < 0.06:
+        return 1
+    elif f < 0.12:
+        return 2
+    elif f < 0.25:
+        return 3
+    elif f < 0.4:
+        return 4
+    else:
+        return 5
+
+
+def calc_repair_level(option_name):
+    if "无维修" in option_name:
+        return 0
+
+    if "主板" in option_name:
+        return "core"
+
+    if "屏幕维修" in option_name:
+        return "important"
+
+    if "更换原厂屏" in option_name:
+        return "important"
+
+    if "摄像头" in option_name:
+        return "important"
+
+    if "电池" in option_name:
+        return "minor"
+
+    if "后壳" in option_name:
+        return "minor"
+
+    if "配件" in option_name:
+        return "minor"
+
+    return None
+
+
+def is_special(row):
+    return row["option_key_name"] == "其他功能问题(可多选或不选)"
+
+def main():
+    with conn.cursor() as cur:
+
+        cur.execute("""
+            SELECT
+                r.option_id,
+                r.option_name,
+                r.option_key_name,
+                p.factor
+            FROM release_option r
+            JOIN price_option_factor p
+              ON r.option_id = p.option_id
+        """)
+
+        rows = cur.fetchall()
+
+        for r in rows:
+
+            group_code = get_group_code(r)
+            severity = calc_severity(r["factor"])
+            sub_weight = 0.5 if group_code in ("screen", "body", "function", "hinge") else None
+            special = 1 if is_special(r) else 0
+
+            repair_level = None
+            if group_code == "repair":
+                repair_level = calc_repair_level(r["option_name"])
+                if repair_level:
+                    repair_level = repair_level_map[repair_level]
+            cur.execute("""
+                UPDATE price_option_factor
+                SET
+                    group_code = %s,
+                    severity_level = %s,
+                    sub_weight = %s,
+                    is_special = %s,
+                    repair_level = %s
+                WHERE option_id = %s
+            """, (
+                group_code,
+                severity,
+                sub_weight,
+                special,
+                repair_level,
+                r["option_id"]
+            ))
+
+        conn.commit()
+
+    print("done.")
+
+
+if __name__ == "__main__":
+    main()