estimate.py 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. from fastapi import APIRouter, Request, Depends, Form
  2. from fastapi.responses import HTMLResponse
  3. from sqlalchemy.orm import Session
  4. from sqlalchemy import text
  5. from database import SessionLocal, redis_client
  6. from services.template_service import build_template
  7. from services.price_service import apply_adjust
  8. from fastapi.templating import Jinja2Templates
  9. import json
  10. import uuid
  11. router = APIRouter(prefix="/estimate", tags=["estimate"])
  12. templates = Jinja2Templates(directory="templates")
  13. # ================= DB =================
  14. def get_db():
  15. db = SessionLocal()
  16. try:
  17. yield db
  18. finally:
  19. db.close()
  20. # ================= 获取所有机型 =================
  21. @router.get("/simulate", response_class=HTMLResponse)
  22. def simulate(db: Session = Depends(get_db)):
  23. # 获取所有有模板的机型
  24. machines = db.execute(text("""
  25. SELECT machine_id, name
  26. FROM t_machine
  27. WHERE machine_id IN (SELECT DISTINCT machine_id FROM machine_temp)
  28. """)).fetchall()
  29. html = "<h2>估价模拟</h2>"
  30. html += '<form method="get" action="/estimate/simulate_one">'
  31. # 展示机型选择
  32. html += '<label>选择机型:</label><br>'
  33. html += '<select name="machine_id">'
  34. for machine in machines:
  35. html += f'<option value="{machine.machine_id}">{machine.name}</option>'
  36. html += '</select><br>'
  37. html += '<button type="submit">选择机型</button></form>'
  38. return html
  39. # ================= 获取机型模板和选项 =================
  40. @router.get("/simulate_one_del", response_class=HTMLResponse)
  41. def simulate(
  42. machine_id: int,
  43. db: Session = Depends(get_db)
  44. ):
  45. # 获取机型对应的模板
  46. row = db.execute(text("""
  47. SELECT estimate_packet
  48. FROM machine_temp
  49. WHERE machine_id=:mid
  50. """), {"mid": machine_id}).fetchone()
  51. tpl = json.loads(row.estimate_packet)
  52. html = f"<h2>估价模拟 - {tpl['templateId']}</h2>"
  53. html += '<form method="post" action="/estimate/simulate">'
  54. for step in tpl["template"]:
  55. html += f"<h3>{step['stepName']}</h3>"
  56. for p in step["properties"]:
  57. html += f"<b>{p['name']}</b><br>"
  58. for v in p["values"]:
  59. html += f"""
  60. <label>
  61. <input type="checkbox" name="option_ids" value="{v['valueId']}">
  62. {v['valueText']}
  63. </label><br>
  64. """
  65. html += f"""
  66. <input type="hidden" name="machine_id" value="{machine_id}">
  67. <br>
  68. <button type="submit">开始估价</button>
  69. </form>
  70. """
  71. return html
  72. @router.get("/simulate_one", response_class=HTMLResponse)
  73. def simulate_one_del2(
  74. request: Request,
  75. machine_id: int,
  76. db: Session = Depends(get_db)
  77. ):
  78. row = db.execute(text("""
  79. SELECT m.name AS machine_name,
  80. t.estimate_packet
  81. FROM machine_temp t
  82. JOIN t_machine m ON t.machine_id = m.machine_id
  83. WHERE t.machine_id = :mid
  84. """), {"mid": machine_id}).fetchone()
  85. if not row:
  86. return HTMLResponse("未找到机型模板", status_code=404)
  87. # print("*************row****************")
  88. # print(row)
  89. tpl = json.loads(row.estimate_packet)
  90. return templates.TemplateResponse(
  91. "simulate_one.html",
  92. {
  93. "request": request,
  94. "machine_id": machine_id,
  95. "machine_name": row.machine_name,
  96. "tpl": tpl
  97. }
  98. )
  99. # ================= 提交估价并计算价格 =================
  100. @router.post("/simulate_del", response_class=HTMLResponse)
  101. def simulate_calc(
  102. machine_id: int = Form(...),
  103. option_ids: list[str] = Form([]),
  104. db: Session = Depends(get_db)
  105. ):
  106. # 获取基准价格
  107. base_price_row = db.execute(text("""
  108. SELECT base_price
  109. FROM machine_base_price
  110. WHERE machine_id=:mid
  111. """), {"mid": machine_id}).fetchone()
  112. # base_price = base_price_row.base_price
  113. base_price = base_price_row.base_price if base_price_row else 1000.0
  114. # 获取选项因素
  115. factors = db.execute(text("""
  116. SELECT option_id, factor, absolute_deduct
  117. FROM price_option_factor
  118. WHERE option_id IN :ids
  119. """), {"ids": tuple(option_ids)}).fetchall()
  120. # 计算价格
  121. price = base_price
  122. for f in factors:
  123. if f.factor:
  124. price *= (1-float(f.factor))
  125. if f.absolute_deduct:
  126. price -= float(f.absolute_deduct)
  127. # 逐步应用调节因子
  128. # 1. 获取全局调节因子
  129. global_adjust = db.execute(text("""
  130. SELECT factor FROM price_adjust_factor
  131. WHERE level='global'
  132. """)).fetchone()
  133. if global_adjust:
  134. price *= float(global_adjust.factor)
  135. # 2. 获取品牌调节因子
  136. brand_adjust = db.execute(text("""
  137. SELECT factor FROM price_adjust_factor
  138. WHERE level='brand'
  139. AND ref_id=(
  140. SELECT brand_id FROM t_machine WHERE machine_id=:mid
  141. )
  142. """), {"mid": machine_id}).fetchone()
  143. if brand_adjust:
  144. price *= float(brand_adjust.factor)
  145. # 3. 获取机型调节因子
  146. machine_adjust = db.execute(text("""
  147. SELECT factor FROM price_adjust_factor
  148. WHERE level='machine'
  149. AND ref_id=:mid
  150. """), {"mid": machine_id}).fetchone()
  151. if machine_adjust:
  152. price *= float(machine_adjust.factor)
  153. html = f"""
  154. <h2>估价结果</h2>
  155. 选择项:{",".join(option_ids)}<br>
  156. 估价:{round(price, 2)} 元<br>
  157. <a href="/simulate?machine_id={machine_id}">返回</a>
  158. """
  159. return html
  160. @router.post("/simulate_de3", response_class=HTMLResponse)
  161. def simulate_calc(
  162. request: Request,
  163. machine_id: int = Form(...),
  164. option_ids: list[str] = Form([]),
  165. db: Session = Depends(get_db)
  166. ):
  167. # 机型名称
  168. machine = db.execute(text("""
  169. SELECT name, brand_name
  170. FROM t_machine
  171. WHERE machine_id=:mid
  172. """), {"mid": machine_id}).fetchone()
  173. # 模板
  174. row = db.execute(text("""
  175. SELECT estimate_packet
  176. FROM machine_temp
  177. WHERE machine_id=:mid
  178. """), {"mid": machine_id}).fetchone()
  179. tpl = json.loads(row.estimate_packet)
  180. # ========== 基准价 ==========
  181. base_price_row = db.execute(text("""
  182. SELECT base_price
  183. FROM machine_base_price
  184. WHERE machine_id=:mid
  185. LIMIT 1
  186. """), {"mid": machine_id}).fetchone()
  187. base_price = float(base_price_row.base_price) if base_price_row else 1000.0
  188. price = base_price
  189. detail_rows = []
  190. # ========== 选项扣减 ==========
  191. if option_ids:
  192. factors = db.execute(text("""
  193. SELECT
  194. pf.option_id,
  195. pf.factor,
  196. pf.absolute_deduct,
  197. ro.option_name
  198. FROM price_option_factor pf
  199. LEFT JOIN release_option ro
  200. ON pf.option_id = ro.option_id
  201. WHERE pf.option_id IN :ids
  202. """), {"ids": tuple(option_ids)}).fetchall()
  203. else:
  204. factors = []
  205. for f in factors:
  206. before = price
  207. if f.factor is not None:
  208. price *= float(f.factor)
  209. if f.absolute_deduct is not None:
  210. price -= float(f.absolute_deduct)
  211. detail_rows.append({
  212. "type": "option",
  213. "name": f.option_name or str(f.option_id),
  214. "option_id": f.option_id,
  215. "factor": f.factor,
  216. "absolute_deduct": f.absolute_deduct,
  217. "before": round(before, 2),
  218. "after": round(price, 2)
  219. })
  220. # ========== 全局调节 ==========
  221. global_adjust = db.execute(text("""
  222. SELECT factor
  223. FROM price_adjust_factor
  224. WHERE level='global'
  225. LIMIT 1
  226. """)).fetchone()
  227. if global_adjust:
  228. before = price
  229. price *= float(global_adjust.factor)
  230. detail_rows.append({
  231. "type": "adjust",
  232. "name": "全局调节",
  233. "factor": global_adjust.factor,
  234. "before": round(before, 2),
  235. "after": round(price, 2)
  236. })
  237. # ========== 品牌调节 ==========
  238. brand_adjust = db.execute(text("""
  239. SELECT factor
  240. FROM price_adjust_factor
  241. WHERE level='brand'
  242. AND ref_id=(
  243. SELECT brand_id FROM t_machine WHERE machine_id=:mid
  244. )
  245. LIMIT 1
  246. """), {"mid": machine_id}).fetchone()
  247. if brand_adjust:
  248. before = price
  249. price *= float(brand_adjust.factor)
  250. detail_rows.append({
  251. "type": "adjust",
  252. "name": "品牌调节",
  253. "factor": brand_adjust.factor,
  254. "before": round(before, 2),
  255. "after": round(price, 2)
  256. })
  257. # ========== 机型调节 ==========
  258. machine_adjust = db.execute(text("""
  259. SELECT factor
  260. FROM price_adjust_factor
  261. WHERE level='machine'
  262. AND ref_id=:mid
  263. LIMIT 1
  264. """), {"mid": machine_id}).fetchone()
  265. if machine_adjust:
  266. before = price
  267. price *= float(machine_adjust.factor)
  268. detail_rows.append({
  269. "type": "adjust",
  270. "name": "机型调节",
  271. "factor": machine_adjust.factor,
  272. "before": round(before, 2),
  273. "after": round(price, 2)
  274. })
  275. final_price = round(price, 2)
  276. return templates.TemplateResponse(
  277. "simulate_one.html",
  278. {
  279. "request": request,
  280. "tpl": tpl,
  281. "machine": machine,
  282. "machine_id": machine_id,
  283. "selected_option_ids": option_ids,
  284. "base_price": round(base_price, 2),
  285. "detail_rows": detail_rows,
  286. "final_price": final_price
  287. }
  288. )
  289. @router.get("/simulate_one", response_class=HTMLResponse)
  290. def simulate_one(
  291. request: Request,
  292. machine_id: int,
  293. db: Session = Depends(get_db)
  294. ):
  295. row = db.execute(text("""
  296. SELECT m.name,
  297. t.estimate_packet
  298. FROM machine_temp t
  299. JOIN t_machine m ON t.machine_id = m.machine_id
  300. WHERE t.machine_id = :mid
  301. """), {"mid": machine_id}).fetchone()
  302. tpl = json.loads(row.estimate_packet)
  303. return templates.TemplateResponse(
  304. "simulate_one.html",
  305. {
  306. "request": request,
  307. "machine_id": machine_id,
  308. "machine_name": row.name,
  309. "tpl": tpl,
  310. "result": None
  311. }
  312. )
  313. # ================= 估价 =================
  314. @router.post("/simulate666", response_class=HTMLResponse)
  315. async def simulate_calc(
  316. request: Request,
  317. machine_id: int = Form(...),
  318. db: Session = Depends(get_db)
  319. ):
  320. form = await request.form()
  321. # ---------------- 收集 option ----------------
  322. option_ids = []
  323. for k, v in form.multi_items():
  324. if k.startswith("option_"):
  325. option_ids.append(str(v))
  326. # ---------------- 重新取模板与机型名 ----------------
  327. row = db.execute(text("""
  328. SELECT m.name,
  329. t.estimate_packet
  330. FROM machine_temp t
  331. JOIN t_machine m ON t.machine_id = m.machine_id
  332. WHERE t.machine_id = :mid
  333. """), {"mid": machine_id}).fetchone()
  334. if not row:
  335. return HTMLResponse("机型模板不存在")
  336. tpl = json.loads(row.estimate_packet)
  337. # ---------------- 构造 valueId -> valueText 映射 ----------------
  338. value_name_map = {}
  339. for step in tpl["template"]:
  340. for p in step["properties"]:
  341. for v in p["values"]:
  342. value_name_map[str(v["valueId"])] = v["valueText"]
  343. # ---------------- 基准价 ----------------
  344. base_row = db.execute(text("""
  345. SELECT base_price
  346. FROM machine_base_price
  347. WHERE machine_id=:mid
  348. LIMIT 1
  349. """), {"mid": machine_id}).fetchone()
  350. base_price = float(base_row.base_price) if base_row else 1000.0
  351. # ---------------- 选项因子 ----------------
  352. detail_rows = []
  353. if option_ids:
  354. rows = db.execute(text("""
  355. SELECT option_id, factor, absolute_deduct
  356. FROM price_option_factor
  357. WHERE option_id IN :ids
  358. """), {"ids": tuple(option_ids)}).fetchall()
  359. else:
  360. rows = []
  361. price = base_price
  362. for r in rows:
  363. before = price
  364. if r.factor is not None:
  365. price = price * float(r.factor)
  366. if r.absolute_deduct is not None:
  367. price = price - float(r.absolute_deduct)
  368. detail_rows.append({
  369. "option_id": r.option_id,
  370. "option_name": value_name_map.get(str(r.option_id), str(r.option_id)),
  371. "factor": r.factor,
  372. "absolute": r.absolute_deduct,
  373. "before": round(before, 2),
  374. "after": round(price, 2)
  375. })
  376. # ---------------- 调节因子 ----------------
  377. # 全局
  378. g = db.execute(text("""
  379. SELECT factor FROM price_adjust_factor
  380. WHERE level='global'
  381. LIMIT 1
  382. """)).fetchone()
  383. if g:
  384. before = price
  385. price = price * float(g.factor)
  386. detail_rows.append({
  387. "option_id": "GLOBAL",
  388. "option_name": "全局调节",
  389. "factor": g.factor,
  390. "absolute": None,
  391. "before": round(before, 2),
  392. "after": round(price, 2)
  393. })
  394. # 品牌
  395. b = db.execute(text("""
  396. SELECT factor FROM price_adjust_factor
  397. WHERE level='brand'
  398. AND ref_id=(
  399. SELECT brand_id
  400. FROM t_machine
  401. WHERE machine_id=:mid
  402. )
  403. LIMIT 1
  404. """), {"mid": machine_id}).fetchone()
  405. if b:
  406. before = price
  407. price = price * float(b.factor)
  408. detail_rows.append({
  409. "option_id": "BRAND",
  410. "option_name": "品牌调节",
  411. "factor": b.factor,
  412. "absolute": None,
  413. "before": round(before, 2),
  414. "after": round(price, 2)
  415. })
  416. # 机型
  417. m = db.execute(text("""
  418. SELECT factor FROM price_adjust_factor
  419. WHERE level='machine'
  420. AND ref_id=:mid
  421. LIMIT 1
  422. """), {"mid": machine_id}).fetchone()
  423. if m:
  424. before = price
  425. price = price * float(m.factor)
  426. detail_rows.append({
  427. "option_id": "MACHINE",
  428. "option_name": "机型调节",
  429. "factor": m.factor,
  430. "absolute": None,
  431. "before": round(before, 2),
  432. "after": round(price, 2)
  433. })
  434. return templates.TemplateResponse(
  435. "simulate_one.html",
  436. {
  437. "request": request,
  438. "machine_id": machine_id,
  439. "machine_name": row.name,
  440. "tpl": tpl,
  441. "result": {
  442. "base_price": round(base_price, 2),
  443. "final_price": round(price, 2),
  444. "details": detail_rows,
  445. "selected": option_ids
  446. }
  447. }
  448. )
  449. @router.post("/simulate7", response_class=HTMLResponse)
  450. async def simulate_calc(
  451. request: Request,
  452. machine_id: int = Form(...),
  453. db: Session = Depends(get_db)
  454. ):
  455. form = await request.form()
  456. # 收集所有 option
  457. option_ids = []
  458. for k, v in form.multi_items():
  459. if k.startswith("option_"):
  460. option_ids.append(str(v))
  461. # ---------------- 重新取模板与机型名 ----------------
  462. row = db.execute(text("""
  463. SELECT m.name,
  464. t.estimate_packet
  465. FROM machine_temp t
  466. JOIN t_machine m ON t.machine_id = m.machine_id
  467. WHERE t.machine_id = :mid
  468. """), {"mid": machine_id}).fetchone()
  469. if not row:
  470. return HTMLResponse("机型模板不存在")
  471. tpl = json.loads(row.estimate_packet)
  472. # ---------------- 构造 valueId -> valueText 映射 ----------------
  473. value_name_map = {}
  474. for step in tpl["template"]:
  475. for p in step["properties"]:
  476. for v in p["values"]:
  477. value_name_map[str(v["valueId"])] = v["valueText"]
  478. # ---------------- 基准价 ----------------
  479. base_row = db.execute(text("""
  480. SELECT base_price
  481. FROM machine_base_price
  482. WHERE machine_id=:mid
  483. LIMIT 1
  484. """), {"mid": machine_id}).fetchone()
  485. base_price_input = form.get("base_price")
  486. if base_price_input and str(base_price_input).strip():
  487. base_price = float(base_price_input)
  488. else:
  489. base_price = float(base_row.base_price) if base_row else 1
  490. # ---------------- 选项因子 ----------------
  491. detail_rows = []
  492. if option_ids:
  493. rows = db.execute(text("""
  494. SELECT option_id, factor, absolute_deduct
  495. FROM price_option_factor
  496. WHERE option_id IN :ids
  497. """), {"ids": tuple(option_ids)}).fetchall()
  498. else:
  499. rows = []
  500. price = base_price
  501. total_deduct_rate = 0
  502. detail_rows.append({
  503. "option_id": "GLOBAL",
  504. "option_name": "-- 以下是扣减比例,累加",
  505. "factor": 0,
  506. "absolute": None,
  507. "before": None,
  508. "after": None
  509. })
  510. for r in rows:
  511. before = price
  512. if r.factor:
  513. total_deduct_rate += float(r.factor)
  514. # price = price * (1-float(r.factor))
  515. # if r.absolute_deduct:
  516. # price = price - float(r.absolute_deduct)
  517. detail_rows.append({
  518. "option_id": r.option_id,
  519. "option_name": value_name_map.get(str(r.option_id), str(r.option_id)),
  520. "factor": r.factor,
  521. "absolute": r.absolute_deduct,
  522. "before": round(before, 2),
  523. "after": round(price, 2)
  524. })
  525. if total_deduct_rate:
  526. price = price * (1 - total_deduct_rate)
  527. price = max(price, 0.0)
  528. # ---------------- 调节因子 ----------------
  529. detail_rows.append({
  530. "option_id": "GLOBAL",
  531. "option_name": "-- 以下是调节因子,累乘",
  532. "factor": 0,
  533. "absolute": None,
  534. "before": None,
  535. "after": None
  536. })
  537. # 全局
  538. g = db.execute(text("""
  539. SELECT factor FROM price_adjust_factor
  540. WHERE level='global'
  541. LIMIT 1
  542. """)).fetchone()
  543. if g:
  544. before = price
  545. price = price * float(g.factor)
  546. detail_rows.append({
  547. "option_id": "GLOBAL",
  548. "option_name": "全局调节系数",
  549. "factor": g.factor,
  550. "absolute": None,
  551. "before": round(before, 2),
  552. "after": round(price, 2)
  553. })
  554. # 品牌
  555. b = db.execute(text("""
  556. SELECT factor FROM price_adjust_factor
  557. WHERE level='brand'
  558. AND ref_id=(
  559. SELECT brand_id
  560. FROM t_machine
  561. WHERE machine_id=:mid
  562. )
  563. LIMIT 1
  564. """), {"mid": machine_id}).fetchone()
  565. if b:
  566. before = price
  567. price = price * float(b.factor)
  568. detail_rows.append({
  569. "option_id": "BRAND",
  570. "option_name": "品牌调节系数",
  571. "factor": b.factor,
  572. "absolute": None,
  573. "before": round(before, 2),
  574. "after": round(price, 2)
  575. })
  576. # 机型
  577. m = db.execute(text("""
  578. SELECT factor FROM price_adjust_factor
  579. WHERE level='machine'
  580. AND ref_id=:mid
  581. LIMIT 1
  582. """), {"mid": machine_id}).fetchone()
  583. if m:
  584. before = price
  585. price = price * float(m.factor)
  586. detail_rows.append({
  587. "option_id": "MACHINE",
  588. "option_name": "机型调节系数",
  589. "factor": m.factor,
  590. "absolute": None,
  591. "before": round(before, 2),
  592. "after": round(price, 2)
  593. })
  594. # 重新取模板与机型名
  595. row = db.execute(text("""
  596. SELECT m.name,
  597. t.estimate_packet
  598. FROM machine_temp t
  599. JOIN t_machine m ON t.machine_id = m.machine_id
  600. WHERE t.machine_id = :mid
  601. """), {"mid": machine_id}).fetchone()
  602. tpl = json.loads(row.estimate_packet)
  603. return templates.TemplateResponse(
  604. "simulate_one.html",
  605. {
  606. "request": request,
  607. "machine_id": machine_id,
  608. "machine_name": row.name,
  609. "tpl": tpl,
  610. "result": {
  611. "base_price": round(base_price, 2),
  612. "final_price": round(price, 2),
  613. "details": detail_rows,
  614. "selected": option_ids
  615. }
  616. }
  617. )
  618. @router.post("/simulate", response_class=HTMLResponse)
  619. async def simulate_calc(
  620. request: Request,
  621. machine_id: int = Form(...),
  622. db: Session = Depends(get_db)
  623. ):
  624. form = await request.form()
  625. option_ids = []
  626. for k, v in form.multi_items():
  627. if k.startswith("option_"):
  628. option_ids.append(int(v))
  629. # ---------------- 基准价 ----------------
  630. base_row = db.execute(text("""
  631. SELECT base_price
  632. FROM machine_base_price
  633. WHERE machine_id=:mid
  634. LIMIT 1
  635. """), {"mid": machine_id}).fetchone()
  636. base_price = float(base_row.base_price) if base_row else 1000.0
  637. # ---------------- 读取选项扣减规则 ----------------
  638. rows = []
  639. if option_ids:
  640. rows = db.execute(text("""
  641. SELECT
  642. p.option_id,
  643. p.factor,
  644. p.absolute_deduct,
  645. p.group_code,
  646. p.severity_level,
  647. p.sub_weight,
  648. p.is_special,
  649. p.repair_level,
  650. g.cap_ratio,
  651. g.group_weight
  652. FROM price_option_factor p
  653. LEFT JOIN price_damage_group g
  654. ON p.group_code = g.group_code
  655. WHERE p.option_id IN :ids
  656. """), {"ids": tuple(option_ids)}).fetchall()
  657. # ---------------- 覆盖关系 ----------------
  658. overrides = db.execute(text("""
  659. SELECT
  660. trigger_group_code,
  661. target_group_code,
  662. override_type,
  663. override_value
  664. FROM price_group_override
  665. """)).fetchall()
  666. override_map = defaultdict(list)
  667. for o in overrides:
  668. override_map[o.trigger_group_code].append(o)
  669. # ---------------- 分组 ----------------
  670. groups = defaultdict(list)
  671. specials = []
  672. repairs = []
  673. for r in rows:
  674. if r.repair_level and r.repair_level > 0:
  675. repairs.append(r)
  676. continue
  677. if r.is_special:
  678. specials.append(r)
  679. continue
  680. groups[r.group_code].append(r)
  681. # ---------------- 覆盖处理 ----------------
  682. hit_groups = set(groups.keys())
  683. skip_groups = set()
  684. weight_override = {}
  685. for g in hit_groups:
  686. for o in override_map.get(g, []):
  687. if o.override_type == "skip":
  688. skip_groups.add(o.target_group_code)
  689. elif o.override_type == "weight":
  690. weight_override[o.target_group_code] = float(o.override_value)
  691. # ---------------- 分组扣减 ----------------
  692. total_ratio = 0.0
  693. detail_rows = []
  694. for group_code, items in groups.items():
  695. if group_code in skip_groups:
  696. continue
  697. items = sorted(
  698. items,
  699. key=lambda x: (
  700. float(x.factor or 0),
  701. x.severity_level or 0
  702. ),
  703. reverse=True
  704. )
  705. main = items[0]
  706. group_deduct = float(main.factor or 0)
  707. for sub in items[1:]:
  708. group_deduct += float(sub.factor or 0) * float(sub.sub_weight or 0.3)
  709. cap_ratio = float(main.cap_ratio or 1.0)
  710. cap = float(main.factor or 0) * cap_ratio
  711. group_deduct = min(group_deduct, cap)
  712. group_weight = float(main.group_weight or 1.0)
  713. if group_code in weight_override:
  714. group_weight = weight_override[group_code]
  715. final_group_deduct = group_deduct * group_weight
  716. total_ratio += final_group_deduct
  717. detail_rows.append({
  718. "option_name": f"--{group_code} 组扣减",
  719. "factor": round(final_group_deduct, 4)
  720. })
  721. for it in items:
  722. detail_rows.append({
  723. "option_name": it.option_id,
  724. "factor": it.factor
  725. })
  726. # ---------------- special 处理 ----------------
  727. special_ratio = 0.0
  728. for s in specials:
  729. special_ratio += float(s.factor or 0)
  730. detail_rows.append({
  731. "option_name": f"特殊项:{s.option_id}",
  732. "factor": s.factor
  733. })
  734. # 特殊项封顶(示例:30%)
  735. special_ratio = min(special_ratio, 0.30)
  736. total_ratio += special_ratio
  737. # ---------------- 维修分级 ----------------
  738. repair_ratio = 0.0
  739. max_repair_level = 0
  740. for r in repairs:
  741. repair_ratio += float(r.factor or 0)
  742. max_repair_level = max(max_repair_level, r.repair_level or 0)
  743. detail_rows.append({
  744. "option_name": f"维修项:{r.option_id}",
  745. "factor": r.factor
  746. })
  747. # 维修封顶(示例)
  748. if max_repair_level >= 3:
  749. repair_ratio = max(repair_ratio, 0.30)
  750. elif max_repair_level == 2:
  751. repair_ratio = min(repair_ratio, 0.20)
  752. else:
  753. repair_ratio = min(repair_ratio, 0.10)
  754. total_ratio += repair_ratio
  755. # ---------------- 最终价格 ----------------
  756. total_ratio = min(total_ratio, 0.90)
  757. final_price = base_price * (1 - total_ratio)
  758. # ---------------- 返回 ----------------
  759. row = db.execute(text("""
  760. SELECT m.name, t.estimate_packet
  761. FROM machine_temp t
  762. JOIN t_machine m ON t.machine_id = m.machine_id
  763. WHERE t.machine_id = :mid
  764. """), {"mid": machine_id}).fetchone()
  765. import json
  766. tpl = json.loads(row.estimate_packet)
  767. return templates.TemplateResponse(
  768. "simulate_one.html",
  769. {
  770. "request": request,
  771. "machine_id": machine_id,
  772. "machine_name": row.name,
  773. "tpl": tpl,
  774. "result": {
  775. "base_price": round(base_price, 2),
  776. "final_price": round(final_price, 2),
  777. "total_ratio": round(total_ratio, 4),
  778. "details": detail_rows,
  779. "selected": [str(i) for i in option_ids]
  780. }
  781. }
  782. )
  783. @router.post("/estimate2", response_class=HTMLResponse)
  784. def estimate_submit(
  785. request: Request,
  786. machine_id: int = Form(...),
  787. capacity: str = Form(...),
  788. option_ids: list[int] = Form([]),
  789. db: Session = Depends(get_db)
  790. ):
  791. # ---------- 基础价格 ----------
  792. base_price = db.execute(
  793. text("""
  794. SELECT base_price
  795. FROM machine_base_price
  796. WHERE machine_id=:mid AND capacity=:cap
  797. """),
  798. {"mid": machine_id, "cap": capacity}
  799. ).scalar() or 0
  800. price = float(base_price)
  801. # ---------- 扣减项 ----------
  802. for oid in option_ids:
  803. r = db.execute(
  804. text("""
  805. SELECT factor, absolute_deduct
  806. FROM price_option_factor
  807. WHERE option_id=:oid
  808. """),
  809. {"oid": oid}
  810. ).fetchone()
  811. if r:
  812. price = price * float(r.factor) - float(r.absolute_deduct)
  813. # ---------- 调节系数 ----------
  814. brand_id = db.execute(
  815. text("""
  816. SELECT brand_id
  817. FROM t_machine
  818. WHERE machine_id=:mid
  819. """),
  820. {"mid": machine_id}
  821. ).scalar()
  822. price = apply_adjust(db, machine_id, brand_id, price)
  823. price = round(max(price, 0), 2)
  824. # ---------- 估价版本 ----------
  825. version_no = str(uuid.uuid4())[:8]
  826. db.execute(
  827. text("""
  828. INSERT INTO estimate_record
  829. (machine_id, capacity, option_ids, final_price, version_no)
  830. VALUES (:m, :c, :o, :p, :v)
  831. """),
  832. {
  833. "m": machine_id,
  834. "c": capacity,
  835. "o": ",".join(map(str, option_ids)),
  836. "p": price,
  837. "v": version_no
  838. }
  839. )
  840. db.commit()
  841. # ---------- 模板(Redis 缓存) ----------
  842. cache_key = f"template:{machine_id}"
  843. cached = redis_client.get(cache_key) if redis_client else None
  844. if cached:
  845. template = json.loads(cached)
  846. else:
  847. rows = db.execute(text("SELECT * FROM release_option")).fetchall()
  848. template = build_template(rows)
  849. if redis_client:
  850. redis_client.set(cache_key, json.dumps(template), ex=3600)
  851. # ---------- 重新加载页面 ----------
  852. machines = db.execute(
  853. text("""
  854. SELECT machine_id, name
  855. FROM t_machine
  856. ORDER BY brand_name, name
  857. """)
  858. ).fetchall()
  859. capacities = db.execute(
  860. text("""
  861. SELECT capacity
  862. FROM machine_base_price
  863. WHERE machine_id=:mid
  864. """),
  865. {"mid": machine_id}
  866. ).fetchall()
  867. return request.app.state.templates.TemplateResponse(
  868. "estimate.html",
  869. {
  870. "request": request,
  871. "machines": machines,
  872. "capacities": capacities,
  873. "template": template,
  874. "price": price,
  875. "version": version_no,
  876. "selected_machine": machine_id,
  877. "selected_capacity": capacity
  878. }
  879. )