mengchang 2 місяців тому
батько
коміт
2bcaa68ba4

BIN
backend/__pycache__/main.cpython-310.pyc


+ 3 - 1
backend/main.py

@@ -1,6 +1,8 @@
 from fastapi import FastAPI
 from fastapi.templating import Jinja2Templates
 from routers import admin, estimate, admin_step1, admin_template
+from routers.estimate_select import router as select_router
+
 
 app = FastAPI(title="设备回收后台")
 
@@ -11,7 +13,7 @@ app.include_router(admin.router)
 app.include_router(estimate.router)
 app.include_router(admin_step1.router)
 app.include_router(admin_template.router)
-
+app.include_router(select_router)
 @app.get("/")
 def index():
     return {"msg": "Recycle Admin Running"}

BIN
backend/routers/__pycache__/admin_step1.cpython-310.pyc


BIN
backend/routers/__pycache__/admin_template.cpython-310.pyc


BIN
backend/routers/__pycache__/estimate.cpython-310.pyc


BIN
backend/routers/__pycache__/estimate_select.cpython-310.pyc


+ 4 - 3
backend/routers/admin_step1.py

@@ -1,6 +1,7 @@
 from fastapi import APIRouter, Depends, Form, Request
 from fastapi.responses import HTMLResponse, RedirectResponse
 from sqlalchemy.orm import Session
+from sqlalchemy import text
 from database import SessionLocal
 
 router = APIRouter(prefix="/admin/step1", tags=["step1"])
@@ -16,10 +17,10 @@ def get_db():
 
 @router.get("", response_class=HTMLResponse)
 def step1_page(request: Request, db: Session = Depends(get_db)):
-    rows = db.execute("""
+    rows = db.execute(text("""
         SELECT * FROM step1_attr
-        ORDER BY base_template_id, attr_key, sort_order
-    """).fetchall()
+        ORDER BY attr_key, sort_order
+    """)).fetchall()
 
     html = """
     <h2>Step1 模板配置</h2>

+ 111 - 17
backend/routers/admin_template.py

@@ -82,6 +82,83 @@ def preview_template(
 # ---------------- 页面 ----------------
 @router.get("/generate", response_class=HTMLResponse)
 def generate_page(request: Request, db: Session = Depends(get_db)):
+
+    machines = db.execute(text("""
+        SELECT machine_id, brand_name, name
+        FROM t_machine
+        ORDER BY brand_name, name
+        LIMIT 200
+    """)).fetchall()
+
+    templates = [
+        (99181, "iPhone 国行基础模板"),
+        (99198, "安卓通用模板"),
+        (99197, "折叠屏模板"),
+    ]
+
+    # ✅ 从 step1_attr 读取所有可补充的属性
+    step1_attrs = db.execute(text("""
+        SELECT
+            attr_key,
+            attr_name,
+            MIN(sort_order) AS sort_order
+        FROM step1_attr
+        GROUP BY attr_key, attr_name
+        ORDER BY sort_order
+    """)).fetchall()
+
+    html = """
+    <h2>生成机型模板</h2>
+
+    <form method="post">
+
+      <h3>① 选择机型</h3>
+      <select name="machine_id">
+    """
+
+    for m in machines:
+        html += f'<option value="{m.machine_id}">{m.brand_name} - {m.name}</option>'
+
+    html += """
+      </select>
+
+      <h3>② 选择基础模板</h3>
+      <select name="base_template_id">
+    """
+
+    for tid, tname in templates:
+        html += f'<option value="{tid}">{tid} - {tname}</option>'
+
+    html += """
+      </select>
+
+      <h3>③ Step1 补充配置(从 step1_attr 读取,逗号分隔)</h3>
+    """
+
+    # ✅ 动态输出所有属性输入框
+    for a in step1_attrs:
+        html += f"""
+        <div style="margin-bottom:10px;">
+            {a.attr_name}({a.attr_key}):<br>
+            <input
+                name="manual_{a.attr_key}"
+                placeholder="例如:A,B,C"
+                style="width:300px;"
+            >
+        </div>
+        """
+
+    html += """
+      <button type="submit">④ 生成模板</button>
+    </form>
+    """
+
+    return html
+
+
+
+@router.get("/generate1", response_class=HTMLResponse)
+def generate_page(request: Request, db: Session = Depends(get_db)):
     machines = db.execute(text("""
       SELECT machine_id, brand_name, name
       FROM t_machine
@@ -138,46 +215,62 @@ def generate_page(request: Request, db: Session = Depends(get_db)):
 
 @router.post("/generate")
 def generate_submit(
+    request: Request,
     machine_id: int = Form(...),
     base_template_id: int = Form(...),
-    capacity: str = Form(""),
-    color: str = Form(""),
-    warranty: str = Form(""),
     db: Session = Depends(get_db)
 ):
+    form = request._form  # FastAPI 已解析
+
     # 1️⃣ 基础模板(step1_attr + release_option)
     packet = build_estimate_packet(db, base_template_id)
 
     # 2️⃣ 补充 step1(人工输入)
     step1 = packet[0]["properties"]
 
-    def append_property(key, name, values):
+    # 所有 manual_xxx
+    manual_inputs = {}
+    for k, v in form.items():
+        if k.startswith("manual_"):
+            manual_inputs[k.replace("manual_", "")] = v
+
+    # 读取 step1_attr 的定义(用于取 attr_name)
+    attr_map = db.execute(text("""
+        SELECT attr_key, attr_name
+        FROM step1_attr
+        GROUP BY attr_key, attr_name
+    """)).fetchall()
+
+    attr_name_map = {a.attr_key: a.attr_name for a in attr_map}
+
+    for attr_key, value_str in manual_inputs.items():
+        if not value_str:
+            continue
+
+        values = [x.strip() for x in value_str.split(",") if x.strip()]
         if not values:
-            return
+            continue
+
         step1.append({
-            "id": key,
-            "name": name,
+            "id": attr_key,
+            "name": attr_name_map.get(attr_key, attr_key),
             "required": False,
             "isMulti": False,
             "values": [
                 {
-                    "valueId": f"manual_{v.strip()}",
-                    "valueText": v.strip(),
+                    "valueId": f"manual_{attr_key}_{v}",
+                    "valueText": v,
                     "isNormal": True
                 }
-                for v in values.split(",") if v.strip()
+                for v in values
             ]
         })
 
-    append_property("capacity", "容量", capacity)
-    append_property("color", "颜色", color)
-    append_property("warranty", "保修", warranty)
-
     # 3️⃣ 保存模板
     db.execute(text("""
-      INSERT INTO machine_temp(machine_id,temp_type,estimate_packet)
-      VALUES (:mid,'00',:json)
-      ON DUPLICATE KEY UPDATE estimate_packet=:json
+        INSERT INTO machine_temp(machine_id, temp_type, estimate_packet)
+        VALUES (:mid, '00', :json)
+        ON DUPLICATE KEY UPDATE estimate_packet=:json
     """), {
         "mid": machine_id,
         "json": json.dumps({
@@ -187,4 +280,5 @@ def generate_submit(
     })
 
     db.commit()
+
     return RedirectResponse("/admin/template/generate", status_code=302)

+ 574 - 4
backend/routers/estimate.py

@@ -5,11 +5,12 @@ from sqlalchemy import text
 from database import SessionLocal, redis_client
 from services.template_service import build_template
 from services.price_service import apply_adjust
+from fastapi.templating import Jinja2Templates
 import json
 import uuid
 
 router = APIRouter(prefix="/estimate", tags=["estimate"])
-
+templates = Jinja2Templates(directory="templates")
 
 # ================= DB =================
 def get_db():
@@ -45,7 +46,7 @@ def simulate(db: Session = Depends(get_db)):
 
 
 # ================= 获取机型模板和选项 =================
-@router.get("/simulate_one", response_class=HTMLResponse)
+@router.get("/simulate_one_del", response_class=HTMLResponse)
 def simulate(
     machine_id: int, 
     db: Session = Depends(get_db)
@@ -85,11 +86,42 @@ def simulate(
     """
 
     return html
+@router.get("/simulate_one", response_class=HTMLResponse)
+def simulate_one_del2(
+    request: Request,
+    machine_id: int,
+    db: Session = Depends(get_db)
+):
+
+    row = db.execute(text("""
+        SELECT m.name AS machine_name,
+               t.estimate_packet
+        FROM machine_temp t
+        JOIN t_machine m ON t.machine_id = m.machine_id
+        WHERE t.machine_id = :mid
+    """), {"mid": machine_id}).fetchone()
+
+    if not row:
+        return HTMLResponse("未找到机型模板", status_code=404)
+    
+    # print("*************row****************")
+    # print(row)
+    tpl = json.loads(row.estimate_packet)
+
+    return templates.TemplateResponse(
+        "simulate_one.html",
+        {
+            "request": request,
+            "machine_id": machine_id,
+            "machine_name": row.machine_name,
+            "tpl": tpl
+        }
+    )
 
 
 
 # ================= 提交估价并计算价格 =================
-@router.post("/simulate", response_class=HTMLResponse)
+@router.post("/simulate_del", response_class=HTMLResponse)
 def simulate_calc(
     machine_id: int = Form(...),
     option_ids: list[str] = Form([]),
@@ -116,7 +148,7 @@ def simulate_calc(
 
     for f in factors:
         if f.factor:
-            price *= float(f.factor)
+            price *= (1-float(f.factor))
         if f.absolute_deduct:
             price -= float(f.absolute_deduct)
 
@@ -161,6 +193,544 @@ def simulate_calc(
     """
 
     return html
+@router.post("/simulate_de3", response_class=HTMLResponse)
+def simulate_calc(
+    request: Request,
+    machine_id: int = Form(...),
+    option_ids: list[str] = Form([]),
+    db: Session = Depends(get_db)
+):
+
+    # 机型名称
+    machine = db.execute(text("""
+        SELECT name, brand_name
+        FROM t_machine
+        WHERE machine_id=:mid
+    """), {"mid": machine_id}).fetchone()
+
+    # 模板
+    row = db.execute(text("""
+        SELECT estimate_packet
+        FROM machine_temp
+        WHERE machine_id=:mid
+    """), {"mid": machine_id}).fetchone()
+
+    tpl = json.loads(row.estimate_packet)
+
+    # ========== 基准价 ==========
+    base_price_row = db.execute(text("""
+      SELECT base_price 
+      FROM machine_base_price 
+      WHERE machine_id=:mid
+      LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    base_price = float(base_price_row.base_price) if base_price_row else 1000.0
+
+    price = base_price
+
+    detail_rows = []
+
+    # ========== 选项扣减 ==========
+    if option_ids:
+        factors = db.execute(text("""
+          SELECT 
+            pf.option_id,
+            pf.factor,
+            pf.absolute_deduct,
+            ro.option_name
+          FROM price_option_factor pf
+          LEFT JOIN release_option ro
+            ON pf.option_id = ro.option_id
+          WHERE pf.option_id IN :ids
+        """), {"ids": tuple(option_ids)}).fetchall()
+    else:
+        factors = []
+
+    for f in factors:
+        before = price
+
+        if f.factor is not None:
+            price *= float(f.factor)
+
+        if f.absolute_deduct is not None:
+            price -= float(f.absolute_deduct)
+
+        detail_rows.append({
+            "type": "option",
+            "name": f.option_name or str(f.option_id),
+            "option_id": f.option_id,
+            "factor": f.factor,
+            "absolute_deduct": f.absolute_deduct,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # ========== 全局调节 ==========
+    global_adjust = db.execute(text("""
+        SELECT factor
+        FROM price_adjust_factor
+        WHERE level='global'
+        LIMIT 1
+    """)).fetchone()
+
+    if global_adjust:
+        before = price
+        price *= float(global_adjust.factor)
+
+        detail_rows.append({
+            "type": "adjust",
+            "name": "全局调节",
+            "factor": global_adjust.factor,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # ========== 品牌调节 ==========
+    brand_adjust = db.execute(text("""
+        SELECT factor
+        FROM price_adjust_factor
+        WHERE level='brand'
+          AND ref_id=(
+            SELECT brand_id FROM t_machine WHERE machine_id=:mid
+          )
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if brand_adjust:
+        before = price
+        price *= float(brand_adjust.factor)
+
+        detail_rows.append({
+            "type": "adjust",
+            "name": "品牌调节",
+            "factor": brand_adjust.factor,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # ========== 机型调节 ==========
+    machine_adjust = db.execute(text("""
+        SELECT factor
+        FROM price_adjust_factor
+        WHERE level='machine'
+          AND ref_id=:mid
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if machine_adjust:
+        before = price
+        price *= float(machine_adjust.factor)
+
+        detail_rows.append({
+            "type": "adjust",
+            "name": "机型调节",
+            "factor": machine_adjust.factor,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    final_price = round(price, 2)
+
+    return templates.TemplateResponse(
+        "simulate_one.html",
+        {
+            "request": request,
+            "tpl": tpl,
+            "machine": machine,
+            "machine_id": machine_id,
+            "selected_option_ids": option_ids,
+            "base_price": round(base_price, 2),
+            "detail_rows": detail_rows,
+            "final_price": final_price
+        }
+    )
+
+@router.get("/simulate_one", response_class=HTMLResponse)
+def simulate_one(
+    request: Request,
+    machine_id: int,
+    db: Session = Depends(get_db)
+):
+    row = db.execute(text("""
+        SELECT m.name,
+               t.estimate_packet
+        FROM machine_temp t
+        JOIN t_machine m ON t.machine_id = m.machine_id
+        WHERE t.machine_id = :mid
+    """), {"mid": machine_id}).fetchone()
+
+    tpl = json.loads(row.estimate_packet)
+
+    return templates.TemplateResponse(
+        "simulate_one.html",
+        {
+            "request": request,
+            "machine_id": machine_id,
+            "machine_name": row.name,
+            "tpl": tpl,
+            "result": None
+        }
+    )
+
+
+# ================= 估价 =================
+@router.post("/simulate666", response_class=HTMLResponse)
+async def simulate_calc(
+    request: Request,
+    machine_id: int = Form(...),
+    db: Session = Depends(get_db)
+):
+    form = await request.form()
+
+    # ---------------- 收集 option ----------------
+    option_ids = []
+    for k, v in form.multi_items():
+        if k.startswith("option_"):
+            option_ids.append(str(v))
+
+    # ---------------- 重新取模板与机型名 ----------------
+    row = db.execute(text("""
+        SELECT m.name,
+               t.estimate_packet
+        FROM machine_temp t
+        JOIN t_machine m ON t.machine_id = m.machine_id
+        WHERE t.machine_id = :mid
+    """), {"mid": machine_id}).fetchone()
+
+    if not row:
+        return HTMLResponse("机型模板不存在")
+
+    tpl = json.loads(row.estimate_packet)
+
+    # ---------------- 构造 valueId -> valueText 映射 ----------------
+    value_name_map = {}
+
+    for step in tpl["template"]:
+        for p in step["properties"]:
+            for v in p["values"]:
+                value_name_map[str(v["valueId"])] = v["valueText"]
+
+    # ---------------- 基准价 ----------------
+    base_row = db.execute(text("""
+        SELECT base_price
+        FROM machine_base_price
+        WHERE machine_id=:mid
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    base_price = float(base_row.base_price) if base_row else 1000.0
+
+    # ---------------- 选项因子 ----------------
+    detail_rows = []
+
+    if option_ids:
+        rows = db.execute(text("""
+            SELECT option_id, factor, absolute_deduct
+            FROM price_option_factor
+            WHERE option_id IN :ids
+        """), {"ids": tuple(option_ids)}).fetchall()
+    else:
+        rows = []
+
+    price = base_price
+
+    for r in rows:
+        before = price
+
+        if r.factor is not None:
+            price = price * float(r.factor)
+
+        if r.absolute_deduct is not None:
+            price = price - float(r.absolute_deduct)
+
+        detail_rows.append({
+            "option_id": r.option_id,
+            "option_name": value_name_map.get(str(r.option_id), str(r.option_id)),
+            "factor": r.factor,
+            "absolute": r.absolute_deduct,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # ---------------- 调节因子 ----------------
+
+    # 全局
+    g = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='global'
+        LIMIT 1
+    """)).fetchone()
+
+    if g:
+        before = price
+        price = price * float(g.factor)
+
+        detail_rows.append({
+            "option_id": "GLOBAL",
+            "option_name": "全局调节",
+            "factor": g.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # 品牌
+    b = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='brand'
+          AND ref_id=(
+            SELECT brand_id
+            FROM t_machine
+            WHERE machine_id=:mid
+          )
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if b:
+        before = price
+        price = price * float(b.factor)
+
+        detail_rows.append({
+            "option_id": "BRAND",
+            "option_name": "品牌调节",
+            "factor": b.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # 机型
+    m = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='machine'
+          AND ref_id=:mid
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if m:
+        before = price
+        price = price * float(m.factor)
+
+        detail_rows.append({
+            "option_id": "MACHINE",
+            "option_name": "机型调节",
+            "factor": m.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    return templates.TemplateResponse(
+        "simulate_one.html",
+        {
+            "request": request,
+            "machine_id": machine_id,
+            "machine_name": row.name,
+            "tpl": tpl,
+            "result": {
+                "base_price": round(base_price, 2),
+                "final_price": round(price, 2),
+                "details": detail_rows,
+                "selected": option_ids
+            }
+        }
+    )
+
+@router.post("/simulate", response_class=HTMLResponse)
+async def simulate_calc(
+    request: Request,
+    machine_id: int = Form(...),
+    db: Session = Depends(get_db)
+):
+    form = await request.form()
+
+    # 收集所有 option
+    option_ids = []
+    for k, v in form.multi_items():
+        if k.startswith("option_"):
+            option_ids.append(str(v))
+
+    # ---------------- 重新取模板与机型名 ----------------
+    row = db.execute(text("""
+        SELECT m.name,
+               t.estimate_packet
+        FROM machine_temp t
+        JOIN t_machine m ON t.machine_id = m.machine_id
+        WHERE t.machine_id = :mid
+    """), {"mid": machine_id}).fetchone()
+
+    if not row:
+        return HTMLResponse("机型模板不存在")
+    tpl = json.loads(row.estimate_packet)
+    # ---------------- 构造 valueId -> valueText 映射 ----------------
+    value_name_map = {}
+
+    for step in tpl["template"]:
+        for p in step["properties"]:
+            for v in p["values"]:
+                value_name_map[str(v["valueId"])] = v["valueText"]
+
+    # ---------------- 基准价 ----------------
+    base_row = db.execute(text("""
+        SELECT base_price
+        FROM machine_base_price
+        WHERE machine_id=:mid
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    base_price_input = form.get("base_price")
+
+    if base_price_input and str(base_price_input).strip():
+        base_price = float(base_price_input)
+    else:
+        base_price = float(base_row.base_price) if base_row else 1
+
+    # ---------------- 选项因子 ----------------
+    detail_rows = []
+
+    if option_ids:
+        rows = db.execute(text("""
+            SELECT option_id, factor, absolute_deduct
+            FROM price_option_factor
+            WHERE option_id IN :ids
+        """), {"ids": tuple(option_ids)}).fetchall()
+    else:
+        rows = []
+
+    price = base_price
+
+    total_deduct_rate = 0
+    detail_rows.append({
+            "option_id": "GLOBAL",
+            "option_name": "--   以下是扣减比例,累加",
+            "factor": 0,
+            "absolute": None,
+            "before": None,
+            "after": None
+    })
+    for r in rows:
+        before = price
+
+        if r.factor:
+            total_deduct_rate += float(r.factor)
+            # price = price * (1-float(r.factor))
+
+        # if r.absolute_deduct:
+        #     price = price - float(r.absolute_deduct)
+
+        detail_rows.append({
+            "option_id": r.option_id,
+            "option_name": value_name_map.get(str(r.option_id), str(r.option_id)),
+            "factor": r.factor,
+            "absolute": r.absolute_deduct,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+    if total_deduct_rate:
+        price = price * (1 - total_deduct_rate)
+    price = max(price, 0.0)
+    # ---------------- 调节因子 ----------------
+
+    detail_rows.append({
+            "option_id": "GLOBAL",
+            "option_name": "--   以下是调节因子,累乘",
+            "factor": 0,
+            "absolute": None,
+            "before": None,
+            "after": None
+    })
+
+    # 全局
+    g = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='global'
+        LIMIT 1
+    """)).fetchone()
+
+    if g:
+        before = price
+        price = price * float(g.factor)
+        detail_rows.append({
+            "option_id": "GLOBAL",
+            "option_name": "全局调节系数",
+            "factor": g.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # 品牌
+    b = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='brand'
+          AND ref_id=(
+            SELECT brand_id
+            FROM t_machine
+            WHERE machine_id=:mid
+          )
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if b:
+        before = price
+        price = price * float(b.factor)
+        detail_rows.append({
+            "option_id": "BRAND",
+            "option_name": "品牌调节系数",
+            "factor": b.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # 机型
+    m = db.execute(text("""
+        SELECT factor FROM price_adjust_factor
+        WHERE level='machine'
+          AND ref_id=:mid
+        LIMIT 1
+    """), {"mid": machine_id}).fetchone()
+
+    if m:
+        before = price
+        price = price * float(m.factor)
+        detail_rows.append({
+            "option_id": "MACHINE",
+            "option_name": "机型调节系数",
+            "factor": m.factor,
+            "absolute": None,
+            "before": round(before, 2),
+            "after": round(price, 2)
+        })
+
+    # 重新取模板与机型名
+    row = db.execute(text("""
+        SELECT m.name,
+               t.estimate_packet
+        FROM machine_temp t
+        JOIN t_machine m ON t.machine_id = m.machine_id
+        WHERE t.machine_id = :mid
+    """), {"mid": machine_id}).fetchone()
+
+    tpl = json.loads(row.estimate_packet)
+
+    return templates.TemplateResponse(
+        "simulate_one.html",
+        {
+            "request": request,
+            "machine_id": machine_id,
+            "machine_name": row.name,
+            "tpl": tpl,
+            "result": {
+                "base_price": round(base_price, 2),
+                "final_price": round(price, 2),
+                "details": detail_rows,
+                "selected": option_ids
+            }
+        }
+    )
+
 
 @router.post("/estimate2", response_class=HTMLResponse)
 def estimate_submit(

+ 66 - 0
backend/routers/estimate_select.py

@@ -0,0 +1,66 @@
+from fastapi import APIRouter, Depends, Query, Request
+from fastapi.responses import HTMLResponse
+from sqlalchemy.orm import Session
+from sqlalchemy import text
+from fastapi.templating import Jinja2Templates
+
+from database import SessionLocal
+
+router = APIRouter(prefix="/estimate")
+
+templates = Jinja2Templates(directory="templates")
+
+
+def get_db():
+    db = SessionLocal()
+    try:
+        yield db
+    finally:
+        db.close()
+
+
+@router.get("/select", response_class=HTMLResponse)
+def select_page(
+    request: Request,
+    brand: str | None = Query(None),
+    db: Session = Depends(get_db)
+):
+    # 默认分类 = 手机
+    type_name = "手机"
+
+    # 左侧品牌
+    brands = db.execute(text("""
+        SELECT DISTINCT brand_name
+        FROM t_machine
+        WHERE type_name=:t
+          AND brand_name IS NOT NULL
+        ORDER BY brand_name
+    """), {"t": type_name}).fetchall()
+
+    # 默认选中第一个品牌
+    if not brand and brands:
+        brand = brands[0].brand_name
+
+    # 右侧机型列表(不再分系列)
+    machines = []
+    if brand:
+        machines = db.execute(text("""
+            SELECT id, name,machine_id
+            FROM t_machine
+            WHERE type_name=:t
+              AND brand_name=:b
+            ORDER BY name
+        """), {
+            "t": type_name,
+            "b": brand
+        }).fetchall()
+
+    return templates.TemplateResponse(
+        "estimate_select.html",
+        {
+            "request": request,
+            "brands": brands,
+            "machines": machines,
+            "current_brand": brand
+        }
+    )

+ 1 - 1
backend/templates/adjust.html

@@ -1,4 +1,4 @@
-<h2>调节系数</h2>
+<h2>调节系数管理</h2>
 
 <form method="post" action="/admin/adjust/save">
 级别:

+ 133 - 0
backend/templates/estimate_select.html

@@ -0,0 +1,133 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>选择机型</title>
+<style>
+body{
+    margin:0;
+    font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto;
+    background:#f7f8fa;
+}
+
+.header{
+    padding:12px 14px;
+    background:#fff;
+    border-bottom:1px solid #eee;
+    font-size:16px;
+    font-weight:600;
+}
+
+.tabs{
+    display:flex;
+    gap:10px;
+    padding:10px 12px;
+    background:#fff;
+    border-bottom:1px solid #eee;
+}
+
+.tab{
+    padding:6px 12px;
+    border-radius:16px;
+    background:#f1f3f5;
+    font-size:13px;
+}
+
+.tab.active{
+    background:#e6f0ff;
+    color:#2563eb;
+    font-weight:600;
+}
+
+.main{
+    display:flex;
+    height:calc(100vh - 96px);
+}
+
+.brand-col{
+    width:90px;
+    background:#fff;
+    border-right:1px solid #eee;
+    overflow-y:auto;
+}
+
+.brand-item{
+    padding:12px 8px;
+    text-align:center;
+    font-size:13px;
+    color:#333;
+}
+
+.brand-item a{
+    text-decoration:none;
+    color:inherit;
+}
+
+.brand-item.active{
+    background:#f0f6ff;
+    color:#2563eb;
+    font-weight:600;
+}
+
+.machine-col{
+    flex:1;
+    background:#fff;
+    overflow-y:auto;
+}
+
+.machine-item{
+    padding:12px 14px;
+    border-bottom:1px solid #f1f1f1;
+    font-size:14px;
+}
+
+.machine-item a{
+    text-decoration:none;
+    color:#111;
+}
+
+.machine-item:hover{
+    background:#f8fafc;
+}
+</style>
+</head>
+<body>
+
+<div class="header">选择旧机</div>
+
+<!-- 分类栏(只保留手机高亮,结构和你图一致) -->
+<div class="tabs">
+    <div class="tab active">手机</div>
+    <div class="tab">平板</div>
+    <div class="tab">笔记本</div>
+    <div class="tab">手表</div>
+</div>
+
+<div class="main">
+
+    <!-- 左:品牌 -->
+    <div class="brand-col">
+        {% for b in brands %}
+        <div class="brand-item {% if current_brand==b.brand_name %}active{% endif %}">
+            <a href="/estimate/select?brand={{b.brand_name}}">
+                {{ b.brand_name }}
+            </a>
+        </div>
+        {% endfor %}
+    </div>
+
+    <!-- 右:机型 -->
+    <div class="machine-col">
+        {% for m in machines %}
+        <div class="machine-item">
+            <a href="/estimate/simulate_one?machine_id={{m.machine_id}}">
+                {{ m.name }}
+            </a>
+        </div>
+        {% endfor %}
+    </div>
+
+</div>
+
+</body>
+</html>

+ 75 - 0
backend/templates/select_machine.html

@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>选择机型</title>
+<style>
+body{margin:0;font-family:Arial;}
+.container{display:flex;height:100vh;}
+.col{border-right:1px solid #ddd;padding:10px;overflow:auto;}
+.col h3{margin-top:0;}
+.item{padding:6px 8px;margin-bottom:4px;cursor:pointer;}
+.item a{text-decoration:none;color:#333;display:block;}
+.active{background:#e6f0ff;border-radius:4px;}
+.machine{border-bottom:1px solid #eee;padding:8px;}
+</style>
+</head>
+<body>
+
+<h2 style="padding:10px">选择机型</h2>
+
+<div class="container">
+
+<!-- 分类 -->
+<div class="col" style="width:120px;">
+<h3>分类</h3>
+{% for t in types %}
+<div class="item {% if type_name==t.type_name %}active{% endif %}">
+  <a href="/estimate/select?type_name={{t.type_name}}">
+    {{t.type_name}}
+  </a>
+</div>
+{% endfor %}
+</div>
+
+<!-- 品牌 -->
+<div class="col" style="width:120px;">
+<h3>品牌</h3>
+{% for b in brands %}
+<div class="item {% if brand_name==b.brand_name %}active{% endif %}">
+  <a href="/estimate/select?type_name={{type_name}}&brand_name={{b.brand_name}}">
+    {{b.brand_name}}
+  </a>
+</div>
+{% endfor %}
+</div>
+
+<!-- 系列 -->
+<div class="col" style="width:140px;">
+<h3>系列</h3>
+{% for s in series %}
+<div class="item {% if series_name==s.series_name %}active{% endif %}">
+  <a href="/estimate/select?type_name={{type_name}}&brand_name={{brand_name}}&series_name={{s.series_name}}">
+    {{s.series_name}}
+  </a>
+</div>
+{% endfor %}
+</div>
+
+<!-- 机型 -->
+<div class="col" style="flex:1;">
+<h3>机型</h3>
+
+{% for m in machines %}
+<div class="machine">
+  <a href="/estimate/simulate?machine_id={{m.id}}">
+    {{m.name}}
+  </a>
+</div>
+{% endfor %}
+
+</div>
+
+</div>
+</body>
+</html>

+ 191 - 0
backend/templates/simulate_one.html

@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>估价模拟</title>
+<style>
+body{
+    font-family: Arial;
+    background:#f6f7f9;
+}
+.container{
+    width:1100px;
+    margin:20px auto;
+    display:flex;
+    gap:20px;
+}
+.left{
+    width:65%;
+    background:#fff;
+    padding:20px;
+    border-radius:6px;
+}
+.right{
+    width:35%;
+    background:#fff;
+    padding:20px;
+    border-radius:6px;
+}
+.step{
+    margin-bottom:20px;
+}
+.prop{
+    margin-bottom:12px;
+}
+.prop-title{
+    font-weight:bold;
+    margin-bottom:6px;
+}
+.option{
+    display:block;
+    margin-left:10px;
+    margin-bottom:4px;
+}
+h2{
+    margin-top:0;
+}
+table{
+    width:100%;
+    border-collapse: collapse;
+}
+td,th{
+    border:1px solid #ddd;
+    padding:6px;
+    font-size:13px;
+}
+</style>
+</head>
+<body>
+
+<div class="container">
+
+<div class="left">
+
+<h2>{{ machine_name }}</h2>
+
+<form method="post" action="/estimate/simulate">
+
+<input type="hidden" name="machine_id" value="{{ machine_id }}">
+<div class="base-price-box">
+    <label>
+        基准价格(可人工输入):
+        <input
+            type="number"
+            step="0.01"
+            name="base_price"
+            placeholder="人工输入基准价"
+            value="{{ result.base_price if result }}"
+            style="width:140px;"
+        >
+        元
+    </label>
+</div>
+{% for step in tpl.template %}
+
+<div class="step">
+    <h3>{{ step.stepName }}</h3>
+
+    {% for p in step.properties %}
+
+    <div class="prop">
+
+        <div class="prop-title">
+            {{ p.name }}
+            {% if p.required %}
+                <span style="color:red">*</span>
+            {% endif %}
+        </div>
+
+        {% set input_type = "checkbox" if p.isMulti else "radio" %}
+        {% set input_name = "option_" ~ p.id %}
+
+        {% for v in p["values"] %}
+        <label class="option">
+            <input
+                type="{{ input_type }}"
+                name="{{ input_name }}"
+                value="{{ v.valueId }}"
+                {% if result and (v.valueId|string) in result.selected %}
+                    checked
+                {% endif %}
+            >
+            {{ v.valueText }}
+        </label>
+        {% endfor %}
+
+    </div>
+
+    {% endfor %}
+</div>
+
+{% endfor %}
+
+<button type="submit">开始估价</button>
+
+</form>
+
+</div>
+
+<div class="right">
+
+{% if result %}
+
+<h3>价格计算明细</h3>
+
+<p>基准价(没有则为1):{{ result.base_price }} 元</p>
+
+<table>
+<tr>
+    <th>项</th>
+    <th>系数</th>
+<!--     <th>固定扣减</th>
+    <th>变动</th> -->
+</tr>
+
+{% for d in result.details %}
+<tr>
+    <td
+        {% if d.option_name and d.option_name.startswith('--') %}
+            style="color:red;font-weight:bold;"
+        {% endif %}
+    >
+        {{ d.option_name }}
+    </td>
+    <td>
+        {% if d.factor %}
+            {{ d.factor }}
+        {% else %}
+            -
+        {% endif %}
+    </td>
+<!--     <td>
+        {% if d.absolute %}
+            - {{ d.absolute }}
+        {% else %}
+            -
+        {% endif %}
+    </td>
+    <td>
+        {{ d.before }} → {{ d.after }}
+    </td> -->
+</tr>
+{% endfor %}
+
+</table>
+
+<h3 style="margin-top:15px;">
+最终价格:{{ result.final_price }} 元
+</h3>
+
+{% else %}
+
+<h3>请选择检测项后点击估价</h3>
+
+{% endif %}
+
+</div>
+
+</div>
+
+</body>
+</html>

+ 34 - 0
mysql/init.sql

@@ -19,6 +19,8 @@ CREATE TABLE t_machine (
   brand_name VARCHAR(100),
   machine_id BIGINT,
   name VARCHAR(100),
+  series_id BIGINT NULL COMMENT '系列ID(Mate / P / nova 等)',
+  series_name VARCHAR(100) NULL COMMENT '系列名称',
   shrink_name VARCHAR(100),
   create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
   update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
@@ -96,10 +98,42 @@ CREATE TABLE price_option_factor (
   option_id BIGINT,
   factor DECIMAL(5,4) DEFAULT 1.0000,
   absolute_deduct DECIMAL(10,2) DEFAULT 0,
+  group_code VARCHAR(32) NULL COMMENT '损伤分组,如 screen_display / touch / repair / camera',
+  severity_level TINYINT DEFAULT 1 COMMENT '严重等级:1轻 2中 3重(用于排序)',
+  sub_weight DECIMAL(5,2) DEFAULT 0.30 COMMENT '作为次瑕疵时的折扣系数',
+  is_special TINYINT DEFAULT 0 COMMENT '是否特殊选项(用于特殊规则组)',
+  repair_level TINYINT DEFAULT 0 COMMENT '维修分级:0非维修 1次要 2重要 3核心',
+  priority INT DEFAULT 0 COMMENT '覆盖与排序优先级',
   create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
   update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
 );
 
+-- ALTER TABLE price_option_factor
+-- ADD COLUMN group_code VARCHAR(32) NULL COMMENT '损伤分组,如 screen_display / touch / repair / camera',
+-- ADD COLUMN severity_level TINYINT DEFAULT 1 COMMENT '严重等级:1轻 2中 3重(用于排序)',
+-- ADD COLUMN sub_weight DECIMAL(5,2) DEFAULT 0.30 COMMENT '作为次瑕疵时的折扣系数',
+-- ADD COLUMN is_special TINYINT DEFAULT 0 COMMENT '是否特殊选项(用于特殊规则组)',
+-- ADD COLUMN repair_level TINYINT DEFAULT 0 COMMENT '维修分级:0非维修 1次要 2重要 3核心',
+-- ADD COLUMN priority INT DEFAULT 0 COMMENT '覆盖与排序优先级';
+
+
+CREATE TABLE price_damage_group (
+  group_code VARCHAR(32) PRIMARY KEY,
+  group_name VARCHAR(64),
+  cap_ratio DECIMAL(5,2) DEFAULT 1.15 COMMENT '组封顶系数(主瑕疵×ratio)',
+  group_weight DECIMAL(5,2) DEFAULT 1.00 COMMENT '整组权重'
+);
+
+
+CREATE TABLE price_group_override (
+  trigger_group_code VARCHAR(32),
+  target_group_code VARCHAR(32),
+  override_type VARCHAR(16) COMMENT 'skip / weight',
+  override_value DECIMAL(5,2) NULL,
+  PRIMARY KEY (trigger_group_code, target_group_code)
+);
+
+
 /* ================= 调节系数 ================= */
 CREATE TABLE price_adjust_factor (
   id BIGINT AUTO_INCREMENT PRIMARY KEY,

+ 137 - 0
temp/t.txt

@@ -0,0 +1,137 @@
+账号情况
+机身外观
+屏幕外观
+屏幕显示
+触摸
+拍摄
+WiFi/蓝牙
+通话
+面容/指纹
+其他功能问题(需员工检测,可多选或不选)
+整机维修(可多选或不选)
+
+
+
+
+成色情况
+账号情况 *
+ 个人账号可退出
+ 个人账号无法退出
+机身外观 *
+ 全新机未拆封(质检时会进行拆封)
+ 外壳完美
+ 外壳有细微划痕
+ 外壳有磕碰掉漆
+ 外壳缺失/裂缝/孔变形/翘起/刻字(含镜片碎裂)
+ 机身有弯曲
+屏幕外观 *
+ 屏幕外观完美
+ 屏幕有细微划痕
+ 屏幕较明显划痕
+ 屏幕裂痕/小缺角/脱胶进灰
+功能情况
+屏幕显示
+ 显示完美,无任何异常
+ 显示轻微泛黄/亮点/亮斑(有其中一项)
+ 显示有透图/透字
+ 显示异常(漏液/错乱/闪屏/屏生线/亮度坏)
+ 屏幕全花屏/无法显示
+触摸
+ 触摸正常
+ 触摸异常(延迟/失灵)
+拍摄
+ 拍摄正常
+ 拍摄异常(抖动/模糊/不对焦/分层/颠倒)
+WiFi/蓝牙
+ WiFi/蓝牙连接正常
+ WiFi/蓝牙连接异常
+通话
+ 通话正常
+ 通话异常
+面容/指纹
+ 面容/指纹功能正常
+ 面容/指纹功能无法录入或识别
+其他功能问题(需员工检测,可多选或不选)
+ 充电功能异常/充电孔接触不良
+ 距离/光线感应异常
+ 听筒/麦克风/扬声器 异常
+ 指南针/重力感应异常
+ 展示机/资源机/官换机
+整机维修(可多选或不选)
+ 机器无维修痕迹
+ 更换电池/摄像头/外壳/其他配件
+ 更换原厂屏
+ 屏幕维修(更换非原厂屏等)
+ 主板维修/扩容
+
+
+参考上面的选项(其他功能问题,整机维修;这两项是多选,其他是单选)和行业,设计一套商业的估价算法,尽量估价合理
+
+
+
+
+
+
+iphone14
+1662   -  1944
+
+846       -  964
+
+816       =   980
+
+
+
+
+iPhone 13
+
+1354 - 1584
+
+
+577  -     687
+
+777  =     897
+
+
+
+iphone 12
+1081 -  1264
+
+459   -  547
+
+622   =  717
+
+
+
+
+
+开机堂:
+iPhone14
+
+2090       1856  功能零件维修      234
+
+
+iphone13
+
+1730        1518      功能零件维修  212
+
+
+iphone12
+
+1310          1165      功能零件维修  145
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+