feat(storefront): purchase popup shows live price - calc_price.dspy preview endpoint + changed-event refresh on storage_gb/valid_months/charge_mode (same cost basis as purchase_realtime)
This commit is contained in:
parent
e870de8985
commit
b0b74a2b9f
@ -147,6 +147,7 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/storefront/api/purchase.dspy",
|
||||
f"/{MOD}/storefront/api/purchase_confirm.dspy",
|
||||
f"/{MOD}/storefront/api/purchase_realtime.dspy",
|
||||
f"/{MOD}/storefront/api/calc_price.dspy",
|
||||
f"/{MOD}/storefront/api/my_subscriptions.dspy",
|
||||
|
||||
# CRUD auto-generated .dspy (new tables)
|
||||
|
||||
64
wwwroot/storefront/api/calc_price.dspy
Normal file
64
wwwroot/storefront/api/calc_price.dspy
Normal file
@ -0,0 +1,64 @@
|
||||
# 购买弹窗实时算价:按当前输入参数试算售价(与 purchase_realtime 扣款同口径,只读不写)
|
||||
# 存储:storage_gb × valid_months(meter_mode=direct 直购)
|
||||
# 产线:charge_mode month/year(年付=月价×10,months 因子由定价映射解析)
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
product_id = params_kw.get('product_id', '')
|
||||
userid = await get_user()
|
||||
|
||||
if not product_id:
|
||||
result = {'success': False, 'message': '缺少产品ID'}
|
||||
elif not userid:
|
||||
result = {'success': False, 'message': '请先登录'}
|
||||
else:
|
||||
try:
|
||||
userorgid = await get_userorgid()
|
||||
dbname = get_module_dbname('product_management')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT id, product_type FROM product WHERE id=${id}$ AND status='1'",
|
||||
{'id': product_id})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
if not recs:
|
||||
raise Exception('产品不存在或已下架')
|
||||
product_type = getattr(recs[0], 'product_type', '') or ''
|
||||
|
||||
# usage_data 构造与 purchase_realtime 完全一致(同口径预览,所见即所付)
|
||||
usage_data = None
|
||||
if product_type == 'workspace_storage':
|
||||
storage_gb = float(params_kw.get('storage_gb') or 0)
|
||||
valid_months = max(1, int(params_kw.get('valid_months') or 1))
|
||||
if storage_gb <= 0:
|
||||
result = {'success': False, 'message': '请填写购买容量(GB)'}
|
||||
else:
|
||||
usage_data = {'meter_mode': 'direct', 'storage_gb': storage_gb,
|
||||
'valid_months': valid_months}
|
||||
elif product_type == 'pipeline':
|
||||
cm = params_kw.get('charge_mode') or 'month'
|
||||
if cm not in ('month', 'year'):
|
||||
cm = 'month'
|
||||
usage_data = {'charge_mode': cm}
|
||||
else:
|
||||
usage_data = {}
|
||||
|
||||
if usage_data is not None:
|
||||
env = ServerEnv()
|
||||
cost = await env.calculate_product_cost(
|
||||
product_id=product_id, usage_data=usage_data,
|
||||
user_org_id=userorgid)
|
||||
if not cost or cost.get('success') is False:
|
||||
result = {'success': False,
|
||||
'message': (cost or {}).get('message', '定价计算失败')}
|
||||
else:
|
||||
result = {
|
||||
'success': True,
|
||||
'amount': float(cost.get('amount', 0) or 0),
|
||||
'original_amount': float(cost.get('original_amount', 0) or 0),
|
||||
'discount': float(cost.get('discount', 1.0) or 1.0),
|
||||
'product_type': product_type,
|
||||
}
|
||||
except Exception as e:
|
||||
debug(f'calc_price error: {format_exc()}')
|
||||
result = {'success': False, 'message': str(e)[:200]}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -50,12 +50,86 @@ else:
|
||||
|
||||
rows.append(_row('产品名称', getattr(p, 'product_name', ''), '#222', True))
|
||||
|
||||
# ── 实时算价(2026-09-08 用户定夺:弹窗显示价格,时长/容量/周期变化实时更新)──
|
||||
# 初始价服务端算好(与 purchase_realtime 扣款同口径:calculate_product_cost
|
||||
# 含客户折扣,所见即所付);输入变化时前端 fetch calc_price.dspy 刷新。
|
||||
env = ServerEnv()
|
||||
init_usage = None
|
||||
if product_type == 'workspace_storage':
|
||||
init_usage = {'meter_mode': 'direct', 'storage_gb': 100, 'valid_months': 1}
|
||||
elif product_type == 'pipeline':
|
||||
init_usage = {'charge_mode': 'month'}
|
||||
init_price_text = '应付金额:—'
|
||||
if init_usage is not None:
|
||||
try:
|
||||
_c = await env.calculate_product_cost(
|
||||
product_id=product_id, usage_data=init_usage,
|
||||
user_org_id=userorgid)
|
||||
if _c and _c.get('success') is not False:
|
||||
_amt = float(_c.get('amount', 0) or 0)
|
||||
_orig = float(_c.get('original_amount', 0) or 0)
|
||||
_disc = float(_c.get('discount', 1.0) or 1.0)
|
||||
init_price_text = '应付金额:¥ %.2f' % _amt
|
||||
if abs(_disc - 1.0) > 1e-9:
|
||||
init_price_text += '(原价 ¥ %.2f × 折扣 %.4g)' % (_orig, _disc)
|
||||
else:
|
||||
init_price_text = '算价失败:%s' % ((_c or {}).get('message', '') or '未知')[:80]
|
||||
except Exception as _pe:
|
||||
init_price_text = '算价失败:%s' % str(_pe)[:80]
|
||||
price_text_widget = {
|
||||
'widgettype': 'Text',
|
||||
'id': 'buy_price_text',
|
||||
'options': {
|
||||
'text': init_price_text,
|
||||
'fontSize': '16px', 'color': '#e74c3c', 'fontWeight': '700',
|
||||
'padding': '10px 0 2px 0'
|
||||
}
|
||||
}
|
||||
|
||||
# 参数输入区(存储需容量+月数;产线选月付/年付;账号走默认月租)
|
||||
# ⚠️ 控件名必须是 bricks 实际注册的:数字输入 = UiInt(不是 InputNumber,
|
||||
# 该名未注册 → widgetBuild 返回 null,控件静默不渲染,2026-09-06 修复)。
|
||||
# UiType 系控件同时传 name + id:name 供 getValue/表单,id 供 getWidgetById 定位
|
||||
# (widgetBuild 用 set_id(desc.id) 覆盖 DOM id)。
|
||||
param_widgets = []
|
||||
|
||||
# 实时刷新价格的共享 script:读当前输入 → fetch calc_price → set_text。
|
||||
# UiInt 的 resultValue() 在 changed 触发时 this.value 已更新,直接读 DOM 控件值最稳。
|
||||
# getWidgetById 必须传第二参数(查找起点),否则 TypeError(2026-09-06 既有 bug)。
|
||||
price_refresh_script = (
|
||||
"var pid='" + product_id + "';"
|
||||
"var pt='" + product_type + "';"
|
||||
"var params=new URLSearchParams();"
|
||||
"params.append('product_id',pid);"
|
||||
"if(pt=='workspace_storage'){"
|
||||
" var gb=bricks.getWidgetById('buy_storage_gb',bricks.app.root);"
|
||||
" var mo=bricks.getWidgetById('buy_valid_months',bricks.app.root);"
|
||||
" var gbv=gb?gb.resultValue():0; var mov=mo?mo.resultValue():1;"
|
||||
" if(gbv&&gbv>0)params.append('storage_gb',gbv);"
|
||||
" if(mov&&mov>0)params.append('valid_months',mov);"
|
||||
"}else if(pt=='pipeline'){"
|
||||
" var cm=bricks.getWidgetById('buy_charge_mode',bricks.app.root);"
|
||||
" params.append('charge_mode',cm?(cm.value||'month'):'month');"
|
||||
"}"
|
||||
"var tw=bricks.getWidgetById('buy_price_text',bricks.app.root);"
|
||||
"if(tw)tw.set_text('计算中...');"
|
||||
"fetch('/product_management/storefront/api/calc_price.dspy',"
|
||||
"{method:'POST',body:params}).then(function(r){return r.json()}).then(function(d){"
|
||||
" var t=bricks.getWidgetById('buy_price_text',bricks.app.root); if(!t)return;"
|
||||
" if(d.success){var s='应付金额:¥ '+(Math.round(d.amount*100)/100).toFixed(2);"
|
||||
" if(Math.abs((d.discount||1)-1)>1e-9){"
|
||||
" s+='(原价 ¥ '+(Math.round(d.original_amount*100)/100).toFixed(2)"
|
||||
" +' × 折扣 '+d.discount+')';}"
|
||||
" t.set_text(s);t.dom_element.style.color='#e74c3c';"
|
||||
" }else{t.set_text('算价失败:'+(d.message||''));t.dom_element.style.color='#e67e22';}"
|
||||
"}).catch(function(e){"
|
||||
" var t=bricks.getWidgetById('buy_price_text',bricks.app.root);"
|
||||
" if(t){t.set_text('算价请求失败:'+e);t.dom_element.style.color='#e67e22';}"
|
||||
"})"
|
||||
)
|
||||
_price_bind = [{'wid': 'self', 'event': 'changed', 'actiontype': 'script',
|
||||
'target': 'self', 'script': price_refresh_script}]
|
||||
|
||||
if product_type == 'workspace_storage':
|
||||
param_widgets.append({
|
||||
'widgettype': 'HBox',
|
||||
@ -64,7 +138,8 @@ else:
|
||||
{'widgettype': 'Text', 'options': {
|
||||
'text': '购买容量(GB)', 'width': '90px', 'fontSize': '13px', 'color': '#666'}},
|
||||
{'widgettype': 'UiInt', 'id': 'buy_storage_gb',
|
||||
'options': {'name': 'buy_storage_gb', 'value': 100, 'width': '120px'}}
|
||||
'options': {'name': 'buy_storage_gb', 'value': 100, 'width': '120px'},
|
||||
'binds': _price_bind}
|
||||
]
|
||||
})
|
||||
param_widgets.append({
|
||||
@ -74,7 +149,8 @@ else:
|
||||
{'widgettype': 'Text', 'options': {
|
||||
'text': '购买月数', 'width': '90px', 'fontSize': '13px', 'color': '#666'}},
|
||||
{'widgettype': 'UiInt', 'id': 'buy_valid_months',
|
||||
'options': {'name': 'buy_valid_months', 'value': 1, 'width': '120px'}}
|
||||
'options': {'name': 'buy_valid_months', 'value': 1, 'width': '120px'},
|
||||
'binds': _price_bind}
|
||||
]
|
||||
})
|
||||
rows.append(_row('计费方式', '按容量直购(元/GB月)', '#666'))
|
||||
@ -89,7 +165,8 @@ else:
|
||||
{'widgettype': 'UiCode', 'id': 'buy_charge_mode',
|
||||
'options': {'name': 'buy_charge_mode', 'value': 'month', 'width': '200px',
|
||||
'data': [{'value': 'month', 'text': '包月(30天)'},
|
||||
{'value': 'year', 'text': '包年(365天,付10个月)'}]}}
|
||||
{'value': 'year', 'text': '包年(365天,付10个月)'}]},
|
||||
'binds': _price_bind}
|
||||
]
|
||||
})
|
||||
rows.append(_row('计费方式', '订阅制使用费(模型/存储消耗另按量计费)', '#666'))
|
||||
@ -188,6 +265,7 @@ else:
|
||||
{'widgettype': 'Title4', 'options': {'text': '确认购买', 'fontWeight': '700', 'marginBottom': '12px'}},
|
||||
{'widgettype': 'VBox', 'options': {'gap': '0', 'marginBottom': '8px'}, 'subwidgets': rows},
|
||||
{'widgettype': 'VBox', 'options': {'gap': '0', 'marginBottom': '8px'}, 'subwidgets': param_widgets},
|
||||
price_text_widget,
|
||||
{
|
||||
'widgettype': 'Text',
|
||||
'options': {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user