estimate.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  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("/simulate", 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("/estimate2", response_class=HTMLResponse)
  619. def estimate_submit(
  620. request: Request,
  621. machine_id: int = Form(...),
  622. capacity: str = Form(...),
  623. option_ids: list[int] = Form([]),
  624. db: Session = Depends(get_db)
  625. ):
  626. # ---------- 基础价格 ----------
  627. base_price = db.execute(
  628. text("""
  629. SELECT base_price
  630. FROM machine_base_price
  631. WHERE machine_id=:mid AND capacity=:cap
  632. """),
  633. {"mid": machine_id, "cap": capacity}
  634. ).scalar() or 0
  635. price = float(base_price)
  636. # ---------- 扣减项 ----------
  637. for oid in option_ids:
  638. r = db.execute(
  639. text("""
  640. SELECT factor, absolute_deduct
  641. FROM price_option_factor
  642. WHERE option_id=:oid
  643. """),
  644. {"oid": oid}
  645. ).fetchone()
  646. if r:
  647. price = price * float(r.factor) - float(r.absolute_deduct)
  648. # ---------- 调节系数 ----------
  649. brand_id = db.execute(
  650. text("""
  651. SELECT brand_id
  652. FROM t_machine
  653. WHERE machine_id=:mid
  654. """),
  655. {"mid": machine_id}
  656. ).scalar()
  657. price = apply_adjust(db, machine_id, brand_id, price)
  658. price = round(max(price, 0), 2)
  659. # ---------- 估价版本 ----------
  660. version_no = str(uuid.uuid4())[:8]
  661. db.execute(
  662. text("""
  663. INSERT INTO estimate_record
  664. (machine_id, capacity, option_ids, final_price, version_no)
  665. VALUES (:m, :c, :o, :p, :v)
  666. """),
  667. {
  668. "m": machine_id,
  669. "c": capacity,
  670. "o": ",".join(map(str, option_ids)),
  671. "p": price,
  672. "v": version_no
  673. }
  674. )
  675. db.commit()
  676. # ---------- 模板(Redis 缓存) ----------
  677. cache_key = f"template:{machine_id}"
  678. cached = redis_client.get(cache_key) if redis_client else None
  679. if cached:
  680. template = json.loads(cached)
  681. else:
  682. rows = db.execute(text("SELECT * FROM release_option")).fetchall()
  683. template = build_template(rows)
  684. if redis_client:
  685. redis_client.set(cache_key, json.dumps(template), ex=3600)
  686. # ---------- 重新加载页面 ----------
  687. machines = db.execute(
  688. text("""
  689. SELECT machine_id, name
  690. FROM t_machine
  691. ORDER BY brand_name, name
  692. """)
  693. ).fetchall()
  694. capacities = db.execute(
  695. text("""
  696. SELECT capacity
  697. FROM machine_base_price
  698. WHERE machine_id=:mid
  699. """),
  700. {"mid": machine_id}
  701. ).fetchall()
  702. return request.app.state.templates.TemplateResponse(
  703. "estimate.html",
  704. {
  705. "request": request,
  706. "machines": machines,
  707. "capacities": capacities,
  708. "template": template,
  709. "price": price,
  710. "version": version_no,
  711. "selected_machine": machine_id,
  712. "selected_capacity": capacity
  713. }
  714. )