estimate.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  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. row = db.execute(text("""
  632. SELECT m.name,
  633. t.estimate_packet
  634. FROM machine_temp t
  635. JOIN t_machine m ON t.machine_id = m.machine_id
  636. WHERE t.machine_id = :mid
  637. """), {"mid": machine_id}).fetchone()
  638. if not row:
  639. return HTMLResponse("机型模板不存在")
  640. tpl = json.loads(row.estimate_packet)
  641. # ---------------- 构造 valueId -> valueText 映射 ----------------
  642. value_name_map = {}
  643. for step in tpl["template"]:
  644. for p in step["properties"]:
  645. for v in p["values"]:
  646. value_name_map[str(v["valueId"])] = v["valueText"]
  647. # ---------------- 基准价 ----------------
  648. base_row = db.execute(text("""
  649. SELECT base_price
  650. FROM machine_base_price
  651. WHERE machine_id=:mid
  652. LIMIT 1
  653. """), {"mid": machine_id}).fetchone()
  654. base_price_input = form.get("base_price")
  655. if base_price_input and str(base_price_input).strip():
  656. base_price = float(base_price_input)
  657. else:
  658. base_price = float(base_row.base_price) if base_row else 1
  659. # base_price = float(base_row.base_price) if base_row else 1000.0
  660. # ---------------- 读取选项扣减规则 ----------------
  661. rows = []
  662. if option_ids:
  663. rows = db.execute(text("""
  664. SELECT
  665. p.option_id,
  666. p.factor,
  667. p.absolute_deduct,
  668. p.group_code,
  669. p.severity_level,
  670. p.sub_weight,
  671. p.is_special,
  672. p.repair_level,
  673. g.cap_ratio,
  674. g.group_weight
  675. FROM price_option_factor p
  676. LEFT JOIN price_damage_group g
  677. ON p.group_code = g.group_code
  678. WHERE p.option_id IN :ids
  679. """), {"ids": tuple(option_ids)}).fetchall()
  680. # ---------------- 覆盖关系 ----------------
  681. overrides = db.execute(text("""
  682. SELECT
  683. trigger_group_code,
  684. target_group_code,
  685. override_type,
  686. override_value
  687. FROM price_group_override
  688. """)).fetchall()
  689. override_map = defaultdict(list)
  690. for o in overrides:
  691. override_map[o.trigger_group_code].append(o)
  692. # ---------------- 分组 ----------------
  693. groups = defaultdict(list)
  694. specials = []
  695. repairs = []
  696. for r in rows:
  697. if r.repair_level and r.repair_level > 0:
  698. repairs.append(r)
  699. continue
  700. if r.is_special:
  701. specials.append(r)
  702. continue
  703. groups[r.group_code].append(r)
  704. # ---------------- 覆盖处理 ----------------
  705. hit_groups = set(groups.keys())
  706. skip_groups = set()
  707. weight_override = {}
  708. for g in hit_groups:
  709. for o in override_map.get(g, []):
  710. if o.override_type == "skip":
  711. skip_groups.add(o.target_group_code)
  712. elif o.override_type == "weight":
  713. weight_override[o.target_group_code] = float(o.override_value)
  714. # ---------------- 分组扣减 ----------------
  715. total_ratio = 0.0
  716. detail_rows = []
  717. for group_code, items in groups.items():
  718. if group_code in skip_groups:
  719. continue
  720. items = sorted(
  721. items,
  722. key=lambda x: (
  723. float(x.factor or 0),
  724. x.severity_level or 0
  725. ),
  726. reverse=True
  727. )
  728. main = items[0]
  729. group_deduct = float(main.factor or 0)
  730. for sub in items[1:]:
  731. group_deduct += float(sub.factor or 0) * float(sub.sub_weight or 0.3)
  732. cap_ratio = float(main.cap_ratio or 1.0)
  733. cap = float(main.factor or 0) * cap_ratio
  734. group_deduct = min(group_deduct, cap)
  735. group_weight = float(main.group_weight or 1.0)
  736. if group_code in weight_override:
  737. group_weight = weight_override[group_code]
  738. final_group_deduct = group_deduct * group_weight
  739. total_ratio += final_group_deduct
  740. detail_rows.append({
  741. "option_name": f"--{group_code} 组扣减",
  742. "factor": round(final_group_deduct, 4)
  743. })
  744. for it in items:
  745. detail_rows.append({
  746. # "option_name": it.option_id,
  747. "option_name": value_name_map.get(str(it.option_id), str(it.option_id)),
  748. "factor": it.factor
  749. })
  750. # ---------------- special 处理 ----------------
  751. special_ratio = 0.0
  752. for s in specials:
  753. special_ratio += float(s.factor or 0)
  754. detail_rows.append({
  755. "option_name": f"特殊项:{s.option_id}",
  756. "factor": s.factor
  757. })
  758. # 特殊项封顶(示例:30%)
  759. special_ratio = min(special_ratio, 0.30)
  760. total_ratio += special_ratio
  761. # ---------------- 维修分级 ----------------
  762. repair_ratio = 0.0
  763. max_repair_level = 0
  764. for r in repairs:
  765. repair_ratio += float(r.factor or 0)
  766. max_repair_level = max(max_repair_level, r.repair_level or 0)
  767. detail_rows.append({
  768. "option_name": f"维修项:{r.option_id}",
  769. "factor": r.factor
  770. })
  771. # 维修封顶(示例)
  772. if max_repair_level >= 3:
  773. repair_ratio = max(repair_ratio, 0.30)
  774. elif max_repair_level == 2:
  775. repair_ratio = min(repair_ratio, 0.20)
  776. else:
  777. repair_ratio = min(repair_ratio, 0.10)
  778. total_ratio += repair_ratio
  779. # ---------------- 最终价格 ----------------
  780. total_ratio = min(total_ratio, 0.90)
  781. final_price = base_price * (1 - total_ratio)
  782. # ---------------- 返回 ----------------
  783. row = db.execute(text("""
  784. SELECT m.name, t.estimate_packet
  785. FROM machine_temp t
  786. JOIN t_machine m ON t.machine_id = m.machine_id
  787. WHERE t.machine_id = :mid
  788. """), {"mid": machine_id}).fetchone()
  789. # import json
  790. tpl = json.loads(row.estimate_packet)
  791. return templates.TemplateResponse(
  792. "simulate_one.html",
  793. {
  794. "request": request,
  795. "machine_id": machine_id,
  796. "machine_name": row.name,
  797. "tpl": tpl,
  798. "result": {
  799. "base_price": round(base_price, 2),
  800. "final_price": round(final_price, 2),
  801. "total_ratio": round(total_ratio, 4),
  802. "details": detail_rows,
  803. "selected": [str(i) for i in option_ids]
  804. }
  805. }
  806. )
  807. SPECIAL_DISCOUNT_RATIO = 0.70 # 特殊项触发后,对剩余价格打7折
  808. REPAIR_LEVEL_CAP = {
  809. 3: 0.60, # 核心维修,最多吃掉基础价60%
  810. 2: 0.35, # 重要维修
  811. 1: 0.15, # 次要维修
  812. }
  813. def calculate_price(db, base_price: float, selected_option_ids: list[int]):
  814. if not selected_option_ids:
  815. return {
  816. "base_price": base_price,
  817. "final_price": base_price,
  818. "details": []
  819. }
  820. # -------------------------------------------------
  821. # 1. 读取 option 规则
  822. # -------------------------------------------------
  823. rows = db.execute(text("""
  824. SELECT
  825. f.option_id,
  826. f.factor,
  827. f.absolute_deduct,
  828. f.group_code,
  829. f.severity_level,
  830. f.sub_weight,
  831. f.is_special,
  832. f.repair_level,
  833. o.option_name
  834. FROM price_option_factor f
  835. LEFT JOIN release_option o
  836. ON o.option_id = f.option_id
  837. WHERE f.option_id IN :ids
  838. """), {
  839. "ids": tuple(selected_option_ids)
  840. }).mappings().all()
  841. # -------------------------------------------------
  842. # 2. 分组
  843. # -------------------------------------------------
  844. group_map = defaultdict(list)
  845. special_items = []
  846. repair_items = []
  847. for r in rows:
  848. r = dict(r)
  849. if r["is_special"]:
  850. special_items.append(r)
  851. if r["repair_level"] and r["repair_level"] > 0:
  852. repair_items.append(r)
  853. group_code = r["group_code"] or "default"
  854. group_map[group_code].append(r)
  855. # -------------------------------------------------
  856. # 3. 读取分组配置
  857. # -------------------------------------------------
  858. group_rows = db.execute(text("""
  859. SELECT
  860. group_code,
  861. cap_ratio,
  862. group_weight
  863. FROM price_damage_group
  864. """)).mappings().all()
  865. group_conf = {r["group_code"]: r for r in group_rows}
  866. # -------------------------------------------------
  867. # 4. 读取组联动
  868. # -------------------------------------------------
  869. override_rows = db.execute(text("""
  870. SELECT
  871. trigger_group_code,
  872. target_group_code,
  873. override_type,
  874. override_value
  875. FROM price_group_override
  876. """)).mappings().all()
  877. overrides = defaultdict(list)
  878. for r in override_rows:
  879. overrides[r["trigger_group_code"]].append(r)
  880. # -------------------------------------------------
  881. # 5. 普通损伤组计算(不含repair组)
  882. # -------------------------------------------------
  883. group_result = {}
  884. for group_code, items in group_map.items():
  885. if group_code == "repair":
  886. continue
  887. effective = [
  888. i for i in items
  889. if (i["factor"] or 0) > 0 or (i["absolute_deduct"] or 0) > 0
  890. ]
  891. if not effective:
  892. continue
  893. effective.sort(
  894. key=lambda x: (
  895. x["severity_level"] or 0,
  896. x["factor"] or 0
  897. ),
  898. reverse=True
  899. )
  900. main = effective[0]
  901. minors = effective[1:]
  902. main_deduct = base_price * (main["factor"] or 0) + (main["absolute_deduct"] or 0)
  903. minor_sum = 0
  904. for m in minors:
  905. w = m["sub_weight"] if m["sub_weight"] is not None else 0.3
  906. minor_sum += base_price * (m["factor"] or 0) * w
  907. raw = main_deduct + minor_sum
  908. conf = group_conf.get(group_code)
  909. if conf:
  910. cap = main_deduct * float(conf["cap_ratio"])
  911. deduct = min(raw, cap)
  912. deduct *= float(conf["group_weight"])
  913. else:
  914. deduct = raw
  915. group_result[group_code] = {
  916. "deduct": deduct,
  917. "main": main,
  918. "has_effect": deduct > 0
  919. }
  920. # -------------------------------------------------
  921. # 6. repair 组分级模型
  922. # -------------------------------------------------
  923. repair_deduct = 0
  924. repair_detail = []
  925. if repair_items:
  926. by_level = defaultdict(list)
  927. for r in repair_items:
  928. by_level[r["repair_level"]].append(r)
  929. for level, items in by_level.items():
  930. items = [
  931. i for i in items
  932. if (i["factor"] or 0) > 0 or (i["absolute_deduct"] or 0) > 0
  933. ]
  934. if not items:
  935. continue
  936. # 同级只取最严重
  937. items.sort(key=lambda x: x["factor"] or 0, reverse=True)
  938. main = items[0]
  939. d = base_price * (main["factor"] or 0) + (main["absolute_deduct"] or 0)
  940. cap_ratio = REPAIR_LEVEL_CAP.get(level, 1.0)
  941. d = min(d, base_price * cap_ratio)
  942. repair_deduct += d
  943. repair_detail.append({
  944. "repair_level": level,
  945. "option_name": main["option_name"],
  946. "deduct": round(d, 2)
  947. })
  948. group_result["repair"] = {
  949. "deduct": repair_deduct,
  950. "main": None,
  951. "has_effect": repair_deduct > 0,
  952. "detail": repair_detail
  953. }
  954. # -------------------------------------------------
  955. # 7. 组联动
  956. # -------------------------------------------------
  957. disabled_groups = set()
  958. group_weight_override = {}
  959. for trigger, rules in overrides.items():
  960. if trigger not in group_result:
  961. continue
  962. if not group_result[trigger]["has_effect"]:
  963. continue
  964. for rule in rules:
  965. target = rule["target_group_code"]
  966. if rule["override_type"] == "skip":
  967. disabled_groups.add(target)
  968. elif rule["override_type"] == "weight":
  969. group_weight_override[target] = float(rule["override_value"])
  970. # -------------------------------------------------
  971. # 8. 汇总普通扣减
  972. # -------------------------------------------------
  973. total_deduct = 0
  974. details = []
  975. for group_code, g in group_result.items():
  976. if group_code in disabled_groups:
  977. continue
  978. d = g["deduct"]
  979. if group_code in group_weight_override:
  980. d *= group_weight_override[group_code]
  981. total_deduct += d
  982. if group_code == "repair":
  983. details.append({
  984. "group": "repair",
  985. "detail": g["detail"],
  986. "group_deduct": round(d, 2)
  987. })
  988. else:
  989. details.append({
  990. "group": group_code,
  991. "option_name": g["main"]["option_name"],
  992. "group_deduct": round(d, 2)
  993. })
  994. price_after_damage = base_price - total_deduct
  995. if price_after_damage < 0:
  996. price_after_damage = 0
  997. # -------------------------------------------------
  998. # 9. 特殊项整体折价
  999. # -------------------------------------------------
  1000. special_applied = []
  1001. if special_items:
  1002. # 你后面可以在这里按 option 决定不同折扣
  1003. price_after_damage *= SPECIAL_DISCOUNT_RATIO
  1004. for s in special_items:
  1005. special_applied.append(s["option_name"])
  1006. final_price = round(price_after_damage, 2)
  1007. return {
  1008. "base_price": round(base_price, 2),
  1009. "final_price": final_price,
  1010. "damage_deduct": round(total_deduct, 2),
  1011. "special_discount_applied": special_applied,
  1012. "details": details
  1013. }
  1014. # curl -sS -X POST "http://127.0.0.1:8002/api/product/bid" -H "Content-Type: application/json" -H "X-API-Key: dHiw0yduUhnpQRV9z30EGpJD8fAne1CJ" -d '{"task_id":2,"task_types":["bid"]}'