estimate.py 33 KB

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