mengchang 2 месяцев назад
Родитель
Сommit
2bcaa68ba4

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


+ 3 - 1
backend/main.py

@@ -1,6 +1,8 @@
 from fastapi import FastAPI
 from fastapi import FastAPI
 from fastapi.templating import Jinja2Templates
 from fastapi.templating import Jinja2Templates
 from routers import admin, estimate, admin_step1, admin_template
 from routers import admin, estimate, admin_step1, admin_template
+from routers.estimate_select import router as select_router
+
 
 
 app = FastAPI(title="设备回收后台")
 app = FastAPI(title="设备回收后台")
 
 
@@ -11,7 +13,7 @@ app.include_router(admin.router)
 app.include_router(estimate.router)
 app.include_router(estimate.router)
 app.include_router(admin_step1.router)
 app.include_router(admin_step1.router)
 app.include_router(admin_template.router)
 app.include_router(admin_template.router)
-
+app.include_router(select_router)
 @app.get("/")
 @app.get("/")
 def index():
 def index():
     return {"msg": "Recycle Admin Running"}
     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 import APIRouter, Depends, Form, Request
 from fastapi.responses import HTMLResponse, RedirectResponse
 from fastapi.responses import HTMLResponse, RedirectResponse
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import Session
+from sqlalchemy import text
 from database import SessionLocal
 from database import SessionLocal
 
 
 router = APIRouter(prefix="/admin/step1", tags=["step1"])
 router = APIRouter(prefix="/admin/step1", tags=["step1"])
@@ -16,10 +17,10 @@ def get_db():
 
 
 @router.get("", response_class=HTMLResponse)
 @router.get("", response_class=HTMLResponse)
 def step1_page(request: Request, db: Session = Depends(get_db)):
 def step1_page(request: Request, db: Session = Depends(get_db)):
-    rows = db.execute("""
+    rows = db.execute(text("""
         SELECT * FROM step1_attr
         SELECT * FROM step1_attr
-        ORDER BY base_template_id, attr_key, sort_order
-    """).fetchall()
+        ORDER BY attr_key, sort_order
+    """)).fetchall()
 
 
     html = """
     html = """
     <h2>Step1 模板配置</h2>
     <h2>Step1 模板配置</h2>

+ 111 - 17
backend/routers/admin_template.py

@@ -82,6 +82,83 @@ def preview_template(
 # ---------------- 页面 ----------------
 # ---------------- 页面 ----------------
 @router.get("/generate", response_class=HTMLResponse)
 @router.get("/generate", response_class=HTMLResponse)
 def generate_page(request: Request, db: Session = Depends(get_db)):
 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("""
     machines = db.execute(text("""
       SELECT machine_id, brand_name, name
       SELECT machine_id, brand_name, name
       FROM t_machine
       FROM t_machine
@@ -138,46 +215,62 @@ def generate_page(request: Request, db: Session = Depends(get_db)):
 
 
 @router.post("/generate")
 @router.post("/generate")
 def generate_submit(
 def generate_submit(
+    request: Request,
     machine_id: int = Form(...),
     machine_id: int = Form(...),
     base_template_id: int = Form(...),
     base_template_id: int = Form(...),
-    capacity: str = Form(""),
-    color: str = Form(""),
-    warranty: str = Form(""),
     db: Session = Depends(get_db)
     db: Session = Depends(get_db)
 ):
 ):
+    form = request._form  # FastAPI 已解析
+
     # 1️⃣ 基础模板(step1_attr + release_option)
     # 1️⃣ 基础模板(step1_attr + release_option)
     packet = build_estimate_packet(db, base_template_id)
     packet = build_estimate_packet(db, base_template_id)
 
 
     # 2️⃣ 补充 step1(人工输入)
     # 2️⃣ 补充 step1(人工输入)
     step1 = packet[0]["properties"]
     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:
         if not values:
-            return
+            continue
+
         step1.append({
         step1.append({
-            "id": key,
-            "name": name,
+            "id": attr_key,
+            "name": attr_name_map.get(attr_key, attr_key),
             "required": False,
             "required": False,
             "isMulti": False,
             "isMulti": False,
             "values": [
             "values": [
                 {
                 {
-                    "valueId": f"manual_{v.strip()}",
-                    "valueText": v.strip(),
+                    "valueId": f"manual_{attr_key}_{v}",
+                    "valueText": v,
                     "isNormal": True
                     "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️⃣ 保存模板
     # 3️⃣ 保存模板
     db.execute(text("""
     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,
         "mid": machine_id,
         "json": json.dumps({
         "json": json.dumps({
@@ -187,4 +280,5 @@ def generate_submit(
     })
     })
 
 
     db.commit()
     db.commit()
+
     return RedirectResponse("/admin/template/generate", status_code=302)
     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 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
+from fastapi.templating import Jinja2Templates
 import json
 import json
 import uuid
 import uuid
 
 
 router = APIRouter(prefix="/estimate", tags=["estimate"])
 router = APIRouter(prefix="/estimate", tags=["estimate"])
-
+templates = Jinja2Templates(directory="templates")
 
 
 # ================= DB =================
 # ================= DB =================
 def get_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(
 def simulate(
     machine_id: int, 
     machine_id: int, 
     db: Session = Depends(get_db)
     db: Session = Depends(get_db)
@@ -85,11 +86,42 @@ def simulate(
     """
     """
 
 
     return html
     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(
 def simulate_calc(
     machine_id: int = Form(...),
     machine_id: int = Form(...),
     option_ids: list[str] = Form([]),
     option_ids: list[str] = Form([]),
@@ -116,7 +148,7 @@ def simulate_calc(
 
 
     for f in factors:
     for f in factors:
         if f.factor:
         if f.factor:
-            price *= float(f.factor)
+            price *= (1-float(f.factor))
         if f.absolute_deduct:
         if f.absolute_deduct:
             price -= float(f.absolute_deduct)
             price -= float(f.absolute_deduct)
 
 
@@ -161,6 +193,544 @@ def simulate_calc(
     """
     """
 
 
     return html
     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)
 @router.post("/estimate2", response_class=HTMLResponse)
 def estimate_submit(
 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">
 <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),
   brand_name VARCHAR(100),
   machine_id BIGINT,
   machine_id BIGINT,
   name VARCHAR(100),
   name VARCHAR(100),
+  series_id BIGINT NULL COMMENT '系列ID(Mate / P / nova 等)',
+  series_name VARCHAR(100) NULL COMMENT '系列名称',
   shrink_name VARCHAR(100),
   shrink_name VARCHAR(100),
   create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
   create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
   update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
   update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
@@ -96,10 +98,42 @@ CREATE TABLE price_option_factor (
   option_id BIGINT,
   option_id BIGINT,
   factor DECIMAL(5,4) DEFAULT 1.0000,
   factor DECIMAL(5,4) DEFAULT 1.0000,
   absolute_deduct DECIMAL(10,2) DEFAULT 0,
   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,
   create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
   update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE 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 (
 CREATE TABLE price_adjust_factor (
   id BIGINT AUTO_INCREMENT PRIMARY KEY,
   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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+