|
|
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Request, Depends, Form
|
|
|
from fastapi.responses import HTMLResponse
|
|
|
from sqlalchemy.orm import Session
|
|
|
from sqlalchemy import text
|
|
|
+from collections import defaultdict
|
|
|
from database import SessionLocal, redis_client
|
|
|
from services.template_service import build_template
|
|
|
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
|
|
|
+ }
|