feat(pipeline): 产线产品化接入 product_management——计费层+产品接口+定价映射表

产线使用费(订阅制)照 account_resource/storage_resource 范式接入产品层:
- models/pipeline_pricing_map.json: pipeline_id × charge_mode(month/year) → ppid
  (月付 months=1/valid_days=30;年付 months=10/valid_days=365,即月费×10 付10个月用12个月)
- pipeline_pricing.py: 定价映射 CRUD + calculate_pipeline_amount
  (buffered_charging(ppid, {months}) 算原价;无定价方案/金额0 均抛异常不静默)
- product_interface.py: 7 个标准接口(load_product_category_product 导入用;
  get_product_display 展示月/年双档价;execute_product_service 开订阅权益)
- init.py load_pipeline_core: 注册 env.product_interface(module_name='pipeline_core')
  + env.pipeline_pricing_map_save/get_pipeline_ppid/calculate_pipeline_amount

资源主表复用 pipelines(status='published' 才可售),不建 usage 表——
订阅购买是同步实时记账(purchase_realtime → product_accounting_generic 写 biz_order)。

用户定稿的商业模型(2026-09-06):账号免费只管并发配额;产线单独包月
(商机100/投标150/开发200,年付=月×10);模型按量加价;存储 1元/GB月。
This commit is contained in:
yumoqing 2026-09-06 16:55:30 +08:00
parent 9f7144ff7e
commit e134cde44e
4 changed files with 418 additions and 0 deletions

View File

@ -0,0 +1,28 @@
{
"summary": [
{
"name": "pipeline_pricing_map",
"title": "产线计费映射pipeline_id × charge_mode → ppid照 acctres_pricing_map 范式)",
"primary": ["id"]
}
],
"fields": [
{"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "pipeline_id", "title": "产线ID(pipelines.id)", "type": "str", "length": 32, "nullable": "no"},
{"name": "charge_mode", "title": "计费方式(month/year)", "type": "str", "length": 32, "nullable": "no"},
{"name": "months", "title": "计费月数(月付=1,年付=10即月费×10)", "type": "int", "nullable": "no", "default": "1"},
{"name": "valid_days", "title": "权益有效天数(月付30/年付365)", "type": "int", "nullable": "no", "default": "30"},
{"name": "ppid", "title": "定价项目ID", "type": "str", "length": 32, "nullable": "yes"},
{"name": "is_default", "title": "缺省方式", "type": "str", "length": 1, "nullable": "yes", "default": "0"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "yes", "default": "active"},
{"name": "org_id", "title": "所属机构", "type": "str", "length": 32, "nullable": "yes"},
{"name": "created_by", "title": "创建人", "type": "str", "length": 32, "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "yes"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
],
"indexes": [
{"name": "idx_ppm_pipeline", "idxtype": "index", "idxfields": ["pipeline_id"]},
{"name": "idx_ppm_mode", "idxtype": "unique", "idxfields": ["pipeline_id", "charge_mode"]},
{"name": "idx_ppm_ppid", "idxtype": "index", "idxfields": ["ppid"]}
]
}

View File

@ -18,5 +18,32 @@ def load_pipeline_core():
"""注册函数到 ServerEnv"""
# 2026-09-04旧 llm 表 CRUD 已移除——
# 模型注册/管理统一收敛到模型治理模块pipeline-llm旧 `llm` 表停用。
# 2026-09-06产线产品化——注册 product_management 资源模块接口
# (照 account_resource/storage_resource 范式env.product_interface 是单例,
# 产品层按 product_category.resource_module 动态 import + 调本函数重设)。
from ahserver.serverenv import ServerEnv
from .product_interface import (
get_product_display, check_product_availability, check_product_consumable,
execute_product_service, execute_product_service_stream,
calculate_product_cost, load_product_category_product)
env = ServerEnv()
env.product_interface = {
'module_name': MODULE_NAME,
'get_product_display': get_product_display,
'check_product_availability': check_product_availability,
'check_product_consumable': check_product_consumable,
'execute_product_service': execute_product_service,
'execute_product_service_stream': execute_product_service_stream,
'calculate_product_cost': calculate_product_cost,
'load_product_category_product': load_product_category_product,
}
# 产线计费函数(订阅购买/定价解析),供产品层与运维使用
from .pipeline_pricing import (
pipeline_pricing_map_save, get_pipeline_ppid, calculate_pipeline_amount)
env.pipeline_pricing_map_save = pipeline_pricing_map_save
env.get_pipeline_ppid = get_pipeline_ppid
env.calculate_pipeline_amount = calculate_pipeline_amount
debug(f'[{MODULE_NAME}] module loaded')
return True

View File

@ -0,0 +1,169 @@
"""pipeline_core 产线计费层 — 产线使用费(订阅制)
account_resource / storage_resource 范式资源产品化五层
资源主表: pipelinesid=bidding_general ppid
定价映射: pipeline_pricing_mappipeline_id × charge_mode ppid
计费: buffered_charging(ppid, {'months': N}) 月费 × N
月付 months=1年付 months=10"年费=月费×10"送2个月
产线不建 usage 订阅购买是同步实时记账purchase_realtime
product_accounting_generic biz_order订单即审计记录
定价单位约定pricing_program_timing YAML
unit_values: {: 1}
pricings: [{price_factors: months, unit_prices: 100, unit: }]
一个 ppid 服务一条产线月付/年付共用 months 因子区分
"""
import datetime
import json
import logging
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
logger = logging.getLogger(__name__)
MODULE_NAME = 'pipeline_core'
RESOURCE_TYPE = 'pipeline' # product_resource.resource_type / product.product_type
def _get_dbname():
env = ServerEnv()
return env.get_module_dbname(MODULE_NAME)
def _now():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _val(rec, key, default=None):
if isinstance(rec, dict):
return rec.get(key, default)
return getattr(rec, key, default)
# ═══════════════════════════════════════════════════════════════════
# 资源主表pipelines只读——产线定义由能力包管理
# ═══════════════════════════════════════════════════════════════════
async def pipeline_res_list(ns=None):
"""可售卖产线列表status='published')。"""
ns = ns or {}
sql = "SELECT * FROM pipelines WHERE status='published'"
params = {}
if ns.get('org_id'):
sql += " AND org_id=${org_id}$"
params['org_id'] = ns['org_id']
sql += " ORDER BY id"
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(sql, params)
await sor.sqlExe("COMMIT", {})
return recs or []
async def get_pipeline_res(pipeline_id):
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(
"SELECT * FROM pipelines WHERE id=${pid}$", {'pid': pipeline_id})
await sor.sqlExe("COMMIT", {})
return recs[0] if recs else None
# ═══════════════════════════════════════════════════════════════════
# 定价映射pipeline_pricing_map
# ═══════════════════════════════════════════════════════════════════
async def pipeline_pricing_map_list(ns=None):
ns = ns or {}
sql = "SELECT * FROM pipeline_pricing_map WHERE 1=1"
params = {}
if ns.get('pipeline_id'):
sql += " AND pipeline_id=${pid}$"
params['pid'] = ns['pipeline_id']
if ns.get('status'):
sql += " AND status=${st}$"
params['st'] = ns['status']
sql += " ORDER BY pipeline_id, charge_mode"
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(sql, params)
await sor.sqlExe("COMMIT", {})
return recs or []
async def get_pipeline_ppid(pipeline_id, charge_mode=None):
"""解析产线定价方案。charge_mode 空时取 is_default='1'
返回 (ppid, months, valid_days)无映射返回 ('', 0, 0)
"""
sql = ("SELECT ppid, months, valid_days FROM pipeline_pricing_map "
"WHERE pipeline_id=${pid}$ AND status='active'")
params = {'pid': pipeline_id}
if charge_mode:
sql += " AND charge_mode=${cm}$"
params['cm'] = charge_mode
else:
sql += " AND is_default='1'"
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(sql, params)
await sor.sqlExe("COMMIT", {})
if recs:
return (_val(recs[0], 'ppid', '') or '',
int(_val(recs[0], 'months', 1) or 1),
int(_val(recs[0], 'valid_days', 30) or 30))
return '', 0, 0
async def pipeline_pricing_map_save(data):
"""幂等 upsert按 pipeline_id+charge_mode 唯一)。"""
data = dict(data or {})
pid = data.get('pipeline_id', '')
cm = data.get('charge_mode', '')
if not pid or not cm:
raise Exception('pipeline_pricing_map_save: 缺 pipeline_id/charge_mode')
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(
"SELECT id FROM pipeline_pricing_map WHERE pipeline_id=${p}$ AND charge_mode=${c}$",
{'p': pid, 'c': cm})
if recs:
data['id'] = _val(recs[0], 'id')
data['updated_at'] = _now()
await sor.U('pipeline_pricing_map', data)
else:
data.setdefault('id', getID())
data.setdefault('status', 'active')
data.setdefault('is_default', '0')
data.setdefault('org_id', '0')
data['created_at'] = _now()
data['updated_at'] = _now()
await sor.C('pipeline_pricing_map', data)
return data['id']
# ═══════════════════════════════════════════════════════════════════
# 计费(订阅购买,同步)
# ═══════════════════════════════════════════════════════════════════
async def calculate_pipeline_amount(pipeline_id, charge_mode='month'):
"""产线使用费原价 = 定价引擎(ppid, {months}) 结果。
返回 (amount, ppid, months, valid_days)无定价方案抛异常不静默0
"""
ppid, months, valid_days = await get_pipeline_ppid(pipeline_id, charge_mode)
if not ppid:
# 未指定 charge_mode 映射时回退缺省
ppid, months, valid_days = await get_pipeline_ppid(pipeline_id)
if not ppid:
raise Exception('产线 %s 未配置定价方案pipeline_pricing_map 无 ppid' % pipeline_id)
env = ServerEnv()
charging = getattr(env, 'buffered_charging', None)
if charging is None:
raise Exception('pricing 模块未加载,无法计算产线使用费')
usages = {'months': months, 'charge_mode': charge_mode}
prices = await charging(ppid, usages)
if prices is None:
raise Exception('产线定价计算返回空ppid=%s' % ppid)
amount = float(sum(getattr(p, 'amount', 0) or 0 for p in prices))
if amount <= 0:
raise Exception('产线定价计算金额为0ppid=%s, months=%s)——检查定价 YAML' % (ppid, months))
return amount, ppid, months, valid_days

View File

@ -0,0 +1,194 @@
"""pipeline_core 产品接口 — 供 product_management 调用(照 account_resource 范式)。
resource_ref_id = pipelines.id bidding_general
定价维度 = charge_modemonth/year映射表 pipeline_pricing_map
产线是订阅制使用费产品付月费/年费获得该产线的使用资格
模型/存储消耗另按量计费各自独立产品互不混淆
"""
from ahserver.serverenv import ServerEnv
from .pipeline_pricing import (
RESOURCE_TYPE,
calculate_pipeline_amount,
get_pipeline_ppid,
get_pipeline_res,
pipeline_pricing_map_list,
pipeline_res_list,
)
async def _resolve_pipeline(resource_ref_id):
p = await get_pipeline_res(resource_ref_id)
if not p:
return None, '产线 %s 不存在' % resource_ref_id
return p, None
async def load_product_category_product(parent_category_id):
"""返回产线类别 + 产线产品,供 product_management 导入。
类别一个产线服务产品 = pipelines 表全部 published 产线
"""
pls = await pipeline_res_list()
if not pls:
return {'success': False, 'error': '没有已发布(published)的产线'}
categories = [{
'source_id': 'pipelines',
'name': '产线服务',
'description': '产线使用费(订阅制,按月/按年)',
'product_type': RESOURCE_TYPE,
'product_type_title': '产线',
'sort_order': 0,
}]
products = []
for i, p in enumerate(pls):
pid = getattr(p, 'id', '')
name = getattr(p, 'name', '') or pid
desc = getattr(p, 'description', '') or ''
products.append({
'source_category_id': 'pipelines',
'resource_ref_id': pid,
'product_code': pid,
'product_name': name,
'product_type': RESOURCE_TYPE,
'brief_intro': (desc or '')[:200],
'sort_order': i * 10,
})
return {'success': True, 'categories': categories, 'products': products}
async def get_product_display(resource_ref_id):
"""产线定价展示(月费/年费两档,含权益有效期)。"""
p, err = await _resolve_pipeline(resource_ref_id)
if err:
return {'success': False, 'message': err}
env = ServerEnv()
texts = []
detail = {}
for cm, label in (('month', '包月'), ('year', '包年')):
try:
amount, ppid, months, valid_days = await calculate_pipeline_amount(
resource_ref_id, cm)
unit = '元/月' if cm == 'month' else '元/年'
texts.append('%s ¥%.2f %s' % (label, amount, unit))
detail[cm] = {'amount': round(amount, 2), 'ppid': ppid,
'months': months, 'valid_days': valid_days}
except Exception as e:
texts.append('%s 定价未配置' % label)
detail[cm] = {'error': str(e)}
return {
'success': True,
'pricing_text': ' | '.join(texts),
'pricing_detail': detail,
'extra_info': {
'pipeline_id': resource_ref_id,
'pipeline_name': getattr(p, 'name', ''),
'pipeline_type': getattr(p, 'pipeline_type', ''),
},
}
async def check_product_availability(resource_ref_id, user_org_id=None):
p, err = await _resolve_pipeline(resource_ref_id)
if err:
return {'available': False, 'reason': err}
if getattr(p, 'status', '') != 'published':
return {'available': False, 'reason': '产线未发布status=%s' % getattr(p, 'status', '')}
return {'available': True, 'reason': ''}
async def check_product_consumable(resource_ref_id, user_id, user_org_id):
"""购买前预检:产线可用 + 定价已配 + 客户余额。"""
avail = await check_product_availability(resource_ref_id, user_org_id)
if not avail.get('available'):
return {'consumable': False, 'reason': avail.get('reason', ''),
'min_balance': 0, 'pricing_available': False}
maps = await pipeline_pricing_map_list({'pipeline_id': resource_ref_id})
pricing_available = any(getattr(m, 'ppid', '') for m in maps)
if not pricing_available:
return {'consumable': False, 'reason': '产线未配置定价方案',
'min_balance': 0, 'pricing_available': False}
env = ServerEnv()
balance = None
if user_org_id and hasattr(env, 'getCustomerBalance'):
try:
from sqlor.dbpools import DBPools
acc_dbname = env.get_module_dbname('accounting')
async with DBPools().sqlorContext(acc_dbname) as sor:
balance = await env.getCustomerBalance(sor, user_org_id)
except Exception:
balance = None # accounting 未接入 → 不拦
if balance is not None and float(balance) <= 0:
return {'consumable': False,
'reason': '客户资金账户余额不足(%.2f),请先充值' % float(balance),
'min_balance': 0, 'pricing_available': True}
return {'consumable': True, 'reason': '',
'min_balance': 0, 'pricing_available': True}
async def execute_product_service(resource_ref_id, user_id, user_org_id, request_data):
"""产线订阅开通。权益 = 该产线的使用资格,有效期按 charge_mode。"""
p, err = await _resolve_pipeline(resource_ref_id)
if err:
return {'success': False, 'message': err, 'status': 'FAILED'}
charge_mode = (request_data or {}).get('charge_mode', '') or 'month'
if charge_mode not in ('month', 'year'):
return {'success': False, 'message': '产线只支持 charge_mode=month/year',
'status': 'FAILED'}
try:
amount, ppid, months, valid_days = await calculate_pipeline_amount(
resource_ref_id, charge_mode)
except Exception as e:
return {'success': False, 'message': str(e), 'status': 'FAILED'}
return {'success': True,
'message': '产线订阅开通成功',
'status': 'SUCCESS',
'amount': round(amount, 4),
'entitlement': {
'charge_mode': charge_mode,
'duration_days': valid_days,
'pipeline_id': resource_ref_id,
'pipeline_name': getattr(p, 'name', ''),
}}
async def execute_product_service_stream(resource_ref_id, user_id, user_org_id, request_data):
"""产线订阅无流式场景,返回单条结果。"""
r = await execute_product_service(resource_ref_id, user_id, user_org_id, request_data)
async def _gen():
yield {'chunk': r, 'usage_data': None, 'done': True, 'error': None}
return _gen()
async def calculate_product_cost(resource_ref_id, usage_data, user_org_id=None):
"""计算产线使用费原价。usage_data 含 charge_modemonth/year
资源层只算真实定价原价客户折扣由产品层按 product_id 计算
"""
p, err = await _resolve_pipeline(resource_ref_id)
if err:
return {'success': False, 'message': err}
usage_data = usage_data or {}
charge_mode = usage_data.get('charge_mode', '') or 'month'
try:
amount, ppid, months, valid_days = await calculate_pipeline_amount(
resource_ref_id, charge_mode)
except Exception as e:
return {'success': False, 'message': str(e)}
return {'success': True, 'amount': round(amount, 6),
'original_amount': round(amount, 6),
'cost': 0.0, 'discount': 1.0, 'pricing_program_id': ppid}