# 购买确认页:算价预览 + 余额显示 + 参数输入 + 确认支付 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)