1) get_customer_price_display 新增(core.py + __init__.py + env 注册)
统一三类产品对外报价口径:显示价 = 资源真实定价 × 客户折扣率
- 产线 pipeline_pricing_map / 存储 storres_pricing_map / 模型 llm_model.ppid
各自解析 ppid → get_pricing_display 取单位价 → × 折扣
- 折扣率解析走 discount 模块 get_min_product_discount(专属优先/'*'兜底/1.0)
>1 加价(存储1.5)、<1 让利(百炼0.9);折扣≠1 时返回 original_text 原价
- 未登录(user_org_id 空)传 '' 只命中 '*' 通用折扣 → 访客可见报价
2) purchase_realtime 加产线分支(product_type=='pipeline')
订阅制:无 usage 表,直接算价→余额预检→product_accounting_generic 实时记账→开订阅权益
charge_mode month/year,entitlement.duration_days 30/365
3) 展示页 index.ui:接 get_customer_price_display;产线卡显示月/年价+原价划线+订阅态文案;
按钮按订阅状态分流(已订阅→进入产线 / 未订阅→立即购买);product_code 兼容
opportunity_general/bidding_general/sdlc_general 与旧 PIPE-* 两套映射
4) 修两个既有 bug(购买页原本就坏):
- InputNumber 控件 bricks 未注册 → widgetBuild 返回 null 静默不渲染,改 UiInt
- 支付脚本 getWidgetById 缺第二参数(查找起点) → from_widget undefined 抛 TypeError
点击支付无反应,补 bricks.app.root;UiType 取值 .value → resultValue()
- UiType 系控件同时传 name(表单/getValue)+id(set_id 覆盖 DOM id 供 getWidgetById)
本地 mock 渲染测试 6 项全过:三类价格/订阅态分流/规划中占位/未登录访客可见价无购买按钮
206 lines
9.6 KiB
Plaintext
206 lines
9.6 KiB
Plaintext
# 购买确认页:算价预览 + 余额显示 + 参数输入 + 确认支付
|
||
from ahserver.serverenv import ServerEnv
|
||
product_id = params_kw.get('product_id', '')
|
||
userid = await get_user()
|
||
result = {'widgettype': 'Text', 'options': {'text': '加载中...'}}
|
||
|
||
if not product_id:
|
||
result = {'widgettype': 'Text', 'options': {'text': '缺少产品ID', 'color': '#e74c3c'}}
|
||
elif not userid:
|
||
result = {'widgettype': 'Text', 'options': {'text': '请先登录后购买', 'color': '#e74c3c'}}
|
||
else:
|
||
userorgid = await get_userorgid()
|
||
dbname = get_module_dbname('product_management')
|
||
|
||
try:
|
||
async with DBPools().sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT * FROM product WHERE id=${id}$ AND status='1'",
|
||
{'id': product_id})
|
||
if not recs:
|
||
raise Exception('产品不存在或已下架')
|
||
p = recs[0]
|
||
product_type = getattr(p, 'product_type', '') or ''
|
||
|
||
# 余额
|
||
balance = None
|
||
try:
|
||
env = ServerEnv()
|
||
acc_dbname = env.get_module_dbname('accounting')
|
||
async with DBPools().sqlorContext(acc_dbname) as asor:
|
||
balance = await env.getCustomerBalance(asor, userorgid)
|
||
except Exception:
|
||
pass
|
||
|
||
rows = []
|
||
|
||
def _row(label, value, color='#333', bold=False):
|
||
return {
|
||
'widgettype': 'HBox',
|
||
'options': {'padding': '8px 0', 'gap': '8px'},
|
||
'subwidgets': [
|
||
{'widgettype': 'Text', 'options': {
|
||
'text': label, 'width': '90px', 'fontSize': '13px',
|
||
'color': '#666', 'fontWeight': '600' if bold else '400'}},
|
||
{'widgettype': 'Text', 'options': {
|
||
'text': str(value), 'fontSize': '13px', 'color': color,
|
||
'fontWeight': '700' if bold else '400'}}
|
||
]
|
||
}
|
||
|
||
rows.append(_row('产品名称', getattr(p, 'product_name', ''), '#222', True))
|
||
|
||
# 参数输入区(存储需容量+月数;产线选月付/年付;账号走默认月租)
|
||
# ⚠️ 控件名必须是 bricks 实际注册的:数字输入 = UiInt(不是 InputNumber,
|
||
# 该名未注册 → widgetBuild 返回 null,控件静默不渲染,2026-09-06 修复)。
|
||
# UiType 系控件同时传 name + id:name 供 getValue/表单,id 供 getWidgetById 定位
|
||
# (widgetBuild 用 set_id(desc.id) 覆盖 DOM id)。
|
||
param_widgets = []
|
||
if product_type == 'workspace_storage':
|
||
param_widgets.append({
|
||
'widgettype': 'HBox',
|
||
'options': {'padding': '8px 0', 'gap': '8px', 'alignItems': 'center'},
|
||
'subwidgets': [
|
||
{'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'}}
|
||
]
|
||
})
|
||
param_widgets.append({
|
||
'widgettype': 'HBox',
|
||
'options': {'padding': '8px 0', 'gap': '8px', 'alignItems': 'center'},
|
||
'subwidgets': [
|
||
{'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'}}
|
||
]
|
||
})
|
||
rows.append(_row('计费方式', '按容量直购(元/GB月)', '#666'))
|
||
elif product_type == 'pipeline':
|
||
# 产线使用费:月付 / 年付(年费=月费×10,即付10个月用12个月)
|
||
param_widgets.append({
|
||
'widgettype': 'HBox',
|
||
'options': {'padding': '8px 0', 'gap': '8px', 'alignItems': 'center'},
|
||
'subwidgets': [
|
||
{'widgettype': 'Text', 'options': {
|
||
'text': '订阅周期', 'width': '90px', 'fontSize': '13px', 'color': '#666'}},
|
||
{'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个月)'}]}}
|
||
]
|
||
})
|
||
rows.append(_row('计费方式', '订阅制使用费(模型/存储消耗另按量计费)', '#666'))
|
||
elif product_type == 'account':
|
||
rows.append(_row('计费方式', '订阅制(月/年/试用)', '#666'))
|
||
else:
|
||
rows.append(_row('计费方式', '按量计费', '#666'))
|
||
|
||
# 余额展示
|
||
if balance is None:
|
||
balance_text = '未开户'
|
||
balance_color = '#e67e22'
|
||
else:
|
||
balance_text = '¥ %.2f' % balance
|
||
balance_color = '#27ae60' if balance > 0 else '#e74c3c'
|
||
rows.append(_row('账户余额', balance_text, balance_color, True))
|
||
|
||
# 确认支付按钮:script+fetch 提交,确保读取输入框当前值
|
||
# ⚠️ getWidgetById 必须传第二参数(查找起点):bricks.getWidgetById(idset, from_widget)
|
||
# 里 get_by_id 直接读 fromw.dom_element,from_widget 为 undefined 会抛 TypeError
|
||
# → 点击支付无任何反应(既有 bug,2026-09-06 修复)。用 bricks.app.root 作起点。
|
||
# UiInt 取值用 resultValue()(返回 int),UiCode 取值用 .value(返回字符串)。
|
||
is_storage = (product_type == 'workspace_storage')
|
||
is_pipeline = (product_type == 'pipeline')
|
||
pay_script = (
|
||
"var pid='" + product_id + "';"
|
||
"var params=new URLSearchParams();"
|
||
"params.append('product_id',pid);"
|
||
)
|
||
if is_storage:
|
||
pay_script += (
|
||
"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():100; var mov=mo?mo.resultValue():1;"
|
||
"if(!gbv||gbv<=0){alert('请输入购买容量');return;}"
|
||
"if(!mov||mov<=0){alert('请输入购买月数');return;}"
|
||
"params.append('storage_gb',gbv);"
|
||
"params.append('valid_months',mov);"
|
||
)
|
||
if is_pipeline:
|
||
pay_script += (
|
||
"var cm=bricks.getWidgetById('buy_charge_mode',bricks.app.root);"
|
||
"var cmv=cm?(cm.value||'month'):'month';"
|
||
"params.append('charge_mode',cmv);"
|
||
)
|
||
pay_script += (
|
||
"var btn=this; btn.disabled=true;"
|
||
"fetch('/product_management/storefront/api/purchase_realtime.dspy',"
|
||
"{method:'POST',body:params}).then(function(r){return r.json()})"
|
||
".then(function(msg){btn.disabled=false;"
|
||
"if(msg.widgettype){bricks.widgetBuild(msg).then(function(w){if(w)w.open()})}"
|
||
"else{alert(msg.message||'支付完成')}})"
|
||
".catch(function(e){btn.disabled=false;alert('支付请求失败: '+e)})"
|
||
)
|
||
|
||
pay_btn = {
|
||
'widgettype': 'Button',
|
||
'options': {
|
||
'label': '确认支付',
|
||
'bgcolor': '#e74c3c',
|
||
'color': '#fff',
|
||
'padding': '10px 36px',
|
||
'borderRadius': '6px',
|
||
'fontWeight': '600',
|
||
'marginTop': '16px'
|
||
},
|
||
'binds': [{
|
||
'wid': 'self',
|
||
'event': 'click',
|
||
'actiontype': 'script',
|
||
'target': 'self',
|
||
'script': pay_script
|
||
}]
|
||
}
|
||
|
||
cancel_btn = {
|
||
'widgettype': 'Button',
|
||
'options': {
|
||
'label': '取消',
|
||
'bgcolor': '#f0f0f0',
|
||
'color': '#666',
|
||
'padding': '10px 24px',
|
||
'borderRadius': '6px',
|
||
'marginTop': '16px'
|
||
},
|
||
'binds': [{
|
||
'wid': 'self', 'event': 'click', 'actiontype': 'script',
|
||
'target': 'self', 'script': 'if(window.closePopup) closePopup()'
|
||
}]
|
||
}
|
||
|
||
result = {
|
||
'widgettype': 'VBox',
|
||
'options': {'width': '100%', 'padding': '16px'},
|
||
'subwidgets': [
|
||
{'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},
|
||
{
|
||
'widgettype': 'Text',
|
||
'options': {
|
||
'text': '支付将从账户余额实时扣款并完成记账,购买成功后权益立即生效。',
|
||
'fontSize': '12px', 'color': '#999', 'marginTop': '8px'}
|
||
},
|
||
{'widgettype': 'HBox', 'options': {'gap': '12px', 'marginTop': '8px'},
|
||
'subwidgets': [pay_btn, cancel_btn]}
|
||
]
|
||
}
|
||
except Exception as e:
|
||
debug(f'purchase_confirm error: {format_exc()}')
|
||
result = {'widgettype': 'Text', 'options': {'text': str(e), 'color': '#e74c3c'}}
|
||
|
||
return json.dumps(result, ensure_ascii=False)
|