product_management/wwwroot/storefront/api/purchase_confirm.dspy
ymq 167d06cd93 feat(purchase): 实时购买(选A购买即记账)+续费延长+到期门禁+我的权益
核心链路:
- purchase_realtime:算价→余额预检→计费写用量→实时复式记账→开权益,失败回滚
- _grant_subscription 续费修复:未到期从 end_date 顺延不丢剩余天数,已到期重算
- check_subscription_valid 到期门禁:挂到 execute_product,到期/未购买阻断使用
- get_my_subscriptions 我的权益列表(到期状态)
- UI:purchase_confirm 确认页(余额+容量/月数输入)、purchase_realtime 支付、
  my_subscriptions 权益列表+续费、usermenu.ui 挂菜单
2026-08-27 10:40:54 +08:00

175 lines
7.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 购买确认页:算价预览 + 余额显示 + 参数输入 + 确认支付
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))
# 参数输入区(存储需容量+月数;账号走默认月租)
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': 'InputNumber', 'id': 'buy_storage_gb',
'options': {'value': 100, 'min': 1, 'step': 10, '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': 'InputNumber', 'id': 'buy_valid_months',
'options': {'value': 1, 'min': 1, 'max': 12, 'step': 1, 'width': '120px'}}
]
})
rows.append(_row('计费方式', '按容量直购(元/GB月)', '#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 提交,确保读取输入框当前值
is_storage = (product_type == 'workspace_storage')
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');"
"var mo=bricks.getWidgetById('buy_valid_months');"
"var gbv=gb?gb.value:100; var mov=mo?mo.value:1;"
"if(!gbv||gbv<=0){alert('请输入购买容量');return;}"
"params.append('storage_gb',gbv);"
"params.append('valid_months',mov);"
)
pay_script += (
"var btn=this; btn.disabled=true;"
"fetch('{{entire_url(\"./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)