first commit
This commit is contained in:
commit
ebd1b00ddd
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
||||
# mymodule-product
|
||||
|
||||
产品模块(产品分类 / 产品管理 / 产品-能力绑定 / 定价策略)
|
||||
|
||||
## 功能
|
||||
- 树形产品分类管理(CRUD)
|
||||
- 产品 CRUD(包含默认定价、meta、view_schema)
|
||||
- 产品与能力绑定(N:N)
|
||||
- 产品定价策略(产品层覆盖能力定价)
|
||||
- 产品上下线(draft/online/offline)
|
||||
|
||||
## 使用
|
||||
1. 将 `models/*.json` 转换为数据库表(可用迁移脚本)。
|
||||
2. 导入 `init/data.json`。
|
||||
3. 在 `ahserver` 启动时调用 `mymodule.init.setup_app(app)` 注册服务。
|
||||
4. 路由示例见 `mymodule/init.py` 注释。
|
||||
|
||||
## 注意
|
||||
- 示例代码使用 `sqlor` 的接口(sor.C/U/R/D/sqlExe)。
|
||||
- 前端 UI 为伪代码(`wwwroot/*.ui`),可用 bricks 渲染。
|
||||
|
||||
62
init/data.json
Normal file
62
init/data.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"product_category": [
|
||||
{"id":"cat_llm","parent_id": null, "name":"大模型产品"},
|
||||
{"id":"cat_compute","parent_id": null, "name":"算力资源"},
|
||||
{"id":"cat_service","parent_id": null, "name":"技术服务"}
|
||||
],
|
||||
"capability": [
|
||||
{
|
||||
"id": "cap_llm_qwen72b",
|
||||
"name": "Qwen-72B",
|
||||
"kind": "llm",
|
||||
"base_pricing": "{\"input_token\":0.00001, \"output_token\":0.00002}",
|
||||
"meta": "{\"endpoint\":\"http://llm-svc.local/v1/chat\",\"model\":\"qwen-72b\"}"
|
||||
},
|
||||
{
|
||||
"id": "cap_gpu_a100",
|
||||
"name": "A100-80G 池",
|
||||
"kind": "compute",
|
||||
"base_pricing": "{\"hourly\":18.5}",
|
||||
"meta": "{\"pool\":\"gpu_a100_pool\"}"
|
||||
},
|
||||
{
|
||||
"id": "cap_rag_milvus",
|
||||
"name": "Milvus RAG",
|
||||
"kind": "rag",
|
||||
"base_pricing": "{\"per_query\":0.002}",
|
||||
"meta": "{\"endpoint\":\"http://rag.local/query\"}"
|
||||
}
|
||||
],
|
||||
"product": [
|
||||
{
|
||||
"id":"prod_qwen_public",
|
||||
"category_id":"cat_llm",
|
||||
"name":"Qwen 公有云对话",
|
||||
"code":"qwen_public",
|
||||
"type":"LLM_CHAT",
|
||||
"summary":"Qwen-72B 在线对话服务",
|
||||
"status":"online",
|
||||
"default_pricing":"{\"input_token\":0.00001, \"output_token\":0.00002}",
|
||||
"meta":"{\"view_schema\":\"chat_view\",\"executor\":{\"type\":\"LLMExecutor\",\"capability_id\":\"cap_llm_qwen72b\"}}"
|
||||
},
|
||||
{
|
||||
"id":"prod_a100_hour",
|
||||
"category_id":"cat_compute",
|
||||
"name":"A100 小时租用",
|
||||
"code":"gpu_a100_hour",
|
||||
"type":"COMPUTE_RESOURCE",
|
||||
"summary":"按小时租用 A100 资源",
|
||||
"status":"online",
|
||||
"meta":"{\"view_schema\":\"compute_detail\",\"executor\":{\"type\":\"ComputeExecutor\",\"capability_id\":\"cap_gpu_a100\"}}"
|
||||
}
|
||||
],
|
||||
"product_capability": [
|
||||
{"id":"bind_qwen_prod", "product_id":"prod_qwen_public", "capability_id":"cap_llm_qwen72b", "config":"{}"},
|
||||
{"id":"bind_a100_prod", "product_id":"prod_a100_hour", "capability_id":"cap_gpu_a100", "config":"{}"}
|
||||
],
|
||||
"pricing_plan": [
|
||||
{"id":"plan_qwen_payg","product_id":"prod_qwen_public","name":"按量计费","type":"payg","plan_detail":"{\"input_token\":0.00001,\"output_token\":0.00002}","is_default":1},
|
||||
{"id":"plan_a100_hour","product_id":"prod_a100_hour","name":"按小时计费","type":"payg","plan_detail":"{\"hourly\":18.5}","is_default":1}
|
||||
]
|
||||
}
|
||||
|
||||
18
init/script.py
Normal file
18
init/script.py
Normal file
@ -0,0 +1,18 @@
|
||||
# init/script.py
|
||||
import json
|
||||
from sqlor.dbpools import DBPools
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
async def init_db():
|
||||
db = DBPools()
|
||||
env = ServerEnv()
|
||||
dbname = env.get_module_dbname()
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
# 这里假设表已由迁移工具建立;这个脚本负责导入初始数据
|
||||
data = json.load(open('init/data.json','r',encoding='utf8'))
|
||||
for table, rows in data.items():
|
||||
for r in rows:
|
||||
# 注意字段类型转换
|
||||
await sor.C(table, r)
|
||||
return True
|
||||
|
||||
24
json/capability.crud.json
Normal file
24
json/capability.crud.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"tblname": "capability",
|
||||
"alias": "capability",
|
||||
"title": "能力管理",
|
||||
"params": {
|
||||
"sortby": ["name"],
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": ["meta", "orgid", "base_pricing"]
|
||||
},
|
||||
|
||||
"editexclouded": ["created_at", "orgid", "updated_at"],
|
||||
|
||||
"subtables": [
|
||||
{
|
||||
"field": "capability_id",
|
||||
"title": "绑定的产品",
|
||||
"subtable": "product_capability",
|
||||
"url": "{{entire_url('product_capability')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
19
json/pricing_plan.crud.json
Normal file
19
json/pricing_plan.crud.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"tblname": "pricing_plan",
|
||||
"alias": "pricing_plan",
|
||||
"title": "定价策略",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": ["orgid", "plan_detail"]
|
||||
},
|
||||
|
||||
"editexclouded": ["orgid", "created_at", "updated_at"],
|
||||
|
||||
"editor": {
|
||||
"binds": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
json/product.crud.json
Normal file
40
json/product.crud.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"tblname": "product",
|
||||
"alias": "product",
|
||||
"title": "产品管理",
|
||||
"params": {
|
||||
"sortby": ["updated_at desc"],
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": ["orgid", "detail", "meta"],
|
||||
"alters": {
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "draft", "text": "草稿"},
|
||||
{"value": "online", "text": "上架"},
|
||||
{"value": "offline", "text": "下架"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"editexclouded": ["orgid", "created_at", "updated_at"],
|
||||
|
||||
"subtables": [
|
||||
{
|
||||
"field": "product_id",
|
||||
"title": "能力绑定",
|
||||
"subtable": "product_capability",
|
||||
"url": "{{entire_url('product_capability')}}"
|
||||
},
|
||||
{
|
||||
"field": "product_id",
|
||||
"title": "定价计划",
|
||||
"subtable": "pricing_plan",
|
||||
"url": "{{entire_url('pricing_plan')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
19
json/product_capability.crud.json
Normal file
19
json/product_capability.crud.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"tblname": "product_capability",
|
||||
"alias": "product_capability",
|
||||
"title": "产品能力绑定",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
|
||||
"browserfields": {
|
||||
"exclouded": ["orgid", "config"]
|
||||
},
|
||||
|
||||
"editexclouded": ["orgid", "created_at"],
|
||||
|
||||
"editor": {
|
||||
"binds": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
json/product_category.crud.json
Normal file
27
json/product_category.crud.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"tblname": "product_category",
|
||||
"alias": "product_category",
|
||||
"uitype": "tree",
|
||||
"title": "产品分类",
|
||||
"params": {
|
||||
"idField": "id",
|
||||
"textField": "name",
|
||||
"parentField": "parent_id",
|
||||
"editable": true,
|
||||
"sortby": ["order_idx", "name"],
|
||||
"browserfields":{
|
||||
"exclouded": [ "orgid" ],
|
||||
"alters":{}
|
||||
},
|
||||
"edit_exclouded_fields": ["orgid", "created_at", "updated_at"],
|
||||
"subtables": [
|
||||
{
|
||||
"field": "category_id",
|
||||
"title": "所属产品",
|
||||
"subtable": "product",
|
||||
"url": "{{entire_url('product')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
27
models/capability.json
Normal file
27
models/capability.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "capability",
|
||||
"title": "能力",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "能力ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{ "name": "orgid", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "", "comments": "所属组织(多租户隔离)" },
|
||||
|
||||
|
||||
{"name": "name", "title": "名称", "type": "str", "length": 128, "nullable": "no"},
|
||||
{"name": "kind", "title": "能力类型", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "base_pricing", "title": "基础定价(JSON)", "type": "text", "nullable": "yes"},
|
||||
{"name": "meta", "title": "元数据", "type": "text", "nullable": "yes"},
|
||||
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "yes"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_kind", "idxtype": "index", "idxfields": ["kind"]}
|
||||
]
|
||||
}
|
||||
|
||||
37
models/pricing_plan.json
Normal file
37
models/pricing_plan.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pricing_plan",
|
||||
"title": "定价策略",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "定价ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{ "name": "orgid", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "", "comments": "所属组织(多租户隔离)" },
|
||||
|
||||
{"name": "product_id", "title": "产品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "方案名称", "type": "str", "length": 128, "nullable": "no"},
|
||||
|
||||
{"name": "type", "title": "方案类型", "type": "str", "length": 32, "nullable": "no", "default": "payg"},
|
||||
{"name": "plan_detail", "title": "方案内容 JSON", "type": "text", "nullable": "yes"},
|
||||
{"name": "is_default", "title": "是否默认方案", "type": "short", "length": 0, "nullable": "yes", "default": "0"},
|
||||
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "yes"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_product", "idxtype": "index", "idxfields": ["product_id"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "product_id",
|
||||
"table": "product",
|
||||
"valuefield": "id",
|
||||
"textfield": "name",
|
||||
"cond": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
43
models/product.json
Normal file
43
models/product.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "product",
|
||||
"title": "产品",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "产品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{ "name": "orgid", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "", "comments": "所属组织(多租户隔离)" },
|
||||
|
||||
{"name": "category_id", "title": "分类ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "产品名称", "type": "str", "length": 128, "nullable": "no"},
|
||||
{"name": "code", "title": "产品编码", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "type", "title": "产品类型", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "summary", "title": "概要简介", "type": "str", "length": 512, "nullable": "yes"},
|
||||
{"name": "detail", "title": "详情", "type": "text", "nullable": "yes"},
|
||||
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "draft"},
|
||||
|
||||
{"name": "default_pricing", "title": "默认定价(JSON)", "type": "text", "nullable": "yes"},
|
||||
{"name": "meta", "title": "元数据", "type": "text", "nullable": "yes"},
|
||||
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "yes"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_category", "idxtype": "index", "idxfields": ["category_id"]},
|
||||
{"name": "uk_code", "idxtype": "unique", "idxfields": ["code"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "category_id",
|
||||
"table": "product_category",
|
||||
"valuefield": "id",
|
||||
"textfield": "name",
|
||||
"cond": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
44
models/product_catability.json
Normal file
44
models/product_catability.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "product_capability",
|
||||
"title": "产品能力绑定",
|
||||
"primary": "id",
|
||||
"catelog": "relation"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{ "name": "orgid", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "", "comments": "所属组织(多租户隔离)" },
|
||||
|
||||
{"name": "product_id", "title": "产品ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "capability_id", "title": "能力ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "config", "title": "绑定配置(JSON)", "type": "text", "nullable": "yes"},
|
||||
|
||||
{"name": "created_at", "title": "绑定时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_product_cap",
|
||||
"idxtype": "unique",
|
||||
"idxfields": ["product_id", "capability_id"]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "product_id",
|
||||
"table": "product",
|
||||
"valuefield": "id",
|
||||
"textfield": "name",
|
||||
"cond": ""
|
||||
},
|
||||
{
|
||||
"field": "capability_id",
|
||||
"table": "capability",
|
||||
"valuefield": "id",
|
||||
"textfield": "name",
|
||||
"cond": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
27
models/product_category.json
Normal file
27
models/product_category.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "product_category",
|
||||
"title": "产品分类",
|
||||
"primary": "id",
|
||||
"catelog": "entity"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{ "name": "orgid", "title": "租户ID", "type": "str", "length": 32, "nullable": "no", "default": "", "comments": "所属组织(多租户隔离)" },
|
||||
|
||||
{"name": "parent_id", "title": "父分类ID", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "name", "title": "分类名称", "type": "str", "length": 128, "nullable": "no"},
|
||||
{"name": "slug", "title": "分类短名", "type": "str", "length": 128, "nullable": "yes"},
|
||||
{"name": "order_idx", "title": "排序号", "type": "long", "length": 0, "nullable": "yes", "default": "0"},
|
||||
{"name": "meta", "title": "扩展元数据", "type": "text", "nullable": "yes"},
|
||||
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "yes"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "yes"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_parent", "idxtype": "index", "idxfields": ["parent_id"]}
|
||||
]
|
||||
}
|
||||
|
||||
20
product/capability_service.py
Normal file
20
product/capability_service.py
Normal file
@ -0,0 +1,20 @@
|
||||
# mymodule/capability_service.py
|
||||
import json
|
||||
import uuid
|
||||
|
||||
class CapabilityService:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def list(self, sor):
|
||||
return await sor.sqlExe("select * from capability order by name", {})
|
||||
|
||||
async def get(self, sor, cid):
|
||||
rows = await sor.sqlExe("select * from capability where id=${cid}$ limit 1", {'cid':cid})
|
||||
return rows[0] if rows else None
|
||||
|
||||
async def create(self, sor, data):
|
||||
cid = data.get('id') or str(uuid.uuid4())
|
||||
await sor.C('capability', {'id':cid, 'name': data['name'], 'kind': data.get('kind'), 'base_pricing': json.dumps(data.get('base_pricing', {})), 'meta': json.dumps(data.get('meta', {}))})
|
||||
return {'id': cid}
|
||||
|
||||
34
product/category_service.py
Normal file
34
product/category_service.py
Normal file
@ -0,0 +1,34 @@
|
||||
# mymodule/category_service.py
|
||||
import json
|
||||
import uuid
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
class CategoryService:
|
||||
def __init__(self, db: DBPools):
|
||||
self.db = db
|
||||
|
||||
async def list(self, sor, params=None):
|
||||
# 返回树形数据,前端按 parent_id 重建树
|
||||
rows = await sor.sqlExe("select * from product_category order by order_idx, created_at", {})
|
||||
return rows
|
||||
|
||||
async def create(self, sor, data):
|
||||
cid = data.get("id") or str(uuid.uuid4())
|
||||
await sor.C('product_category', {'id':cid, 'parent_id': data.get('parent_id'), 'name': data['name'], 'slug': data.get('slug'), 'order_idx': data.get('order_idx',0), 'meta': json.dumps(data.get('meta', {}))})
|
||||
return {'id': cid}
|
||||
|
||||
async def update(self, sor, cid, data):
|
||||
await sor.U('product_category', {'id': cid, 'name': data.get('name'), 'slug': data.get('slug'), 'order_idx': data.get('order_idx',0), 'meta': json.dumps(data.get('meta', {}))})
|
||||
return {'id': cid}
|
||||
|
||||
async def delete(self, sor, cid):
|
||||
# 检查是否有子分类/产品
|
||||
child = await sor.sqlExe("select 1 from product_category where parent_id=${pid}$ limit 1", {'pid':cid})
|
||||
if child:
|
||||
raise Exception("存在子分类,请先删除或移动子分类")
|
||||
prod = await sor.sqlExe("select 1 from product where category_id=${cid}$ limit 1", {'cid':cid})
|
||||
if prod:
|
||||
raise Exception("分类下存在产品,请先删除或移动产品")
|
||||
await sor.D('product_category', {'id':cid})
|
||||
return {'id': cid}
|
||||
|
||||
29
product/pricing_service.py
Normal file
29
product/pricing_service.py
Normal file
@ -0,0 +1,29 @@
|
||||
# mymodule/pricing_service.py
|
||||
import json
|
||||
import uuid
|
||||
|
||||
class PricingService:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def create_plan(self, sor, product_id, data):
|
||||
pid = str(uuid.uuid4())
|
||||
await sor.C('pricing_plan', {
|
||||
'id': pid,
|
||||
'product_id': product_id,
|
||||
'name': data['name'],
|
||||
'type': data.get('type','payg'),
|
||||
'plan_detail': json.dumps(data.get('plan_detail', {})),
|
||||
'is_default': 1 if data.get('is_default') else 0
|
||||
})
|
||||
return {'id': pid}
|
||||
|
||||
async def update_plan(self, sor, plan_id, data):
|
||||
await sor.U('pricing_plan', {
|
||||
'id': plan_id,
|
||||
'name': data.get('name'),
|
||||
'plan_detail': json.dumps(data.get('plan_detail', {})),
|
||||
'is_default': 1 if data.get('is_default') else 0
|
||||
})
|
||||
return {'id': plan_id}
|
||||
|
||||
87
product/product_service.py
Normal file
87
product/product_service.py
Normal file
@ -0,0 +1,87 @@
|
||||
# mymodule/product_service.py
|
||||
import json
|
||||
import uuid
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
class ProductService:
|
||||
def __init__(self, db: DBPools):
|
||||
self.db = db
|
||||
|
||||
async def get_tree(self, sor):
|
||||
# 返回合并的分类与产品(前端直接渲染树)
|
||||
cats = await sor.sqlExe("select * from product_category order by order_idx", {})
|
||||
prods = await sor.sqlExe("select * from product where status != 'deleted' order by name", {})
|
||||
# 简单合并:前端以 category_id 关联
|
||||
return {'categories': cats, 'products': prods}
|
||||
|
||||
async def create(self, sor, data):
|
||||
pid = data.get('id') or str(uuid.uuid4())
|
||||
await sor.C('product', {
|
||||
'id': pid,
|
||||
'category_id': data['category_id'],
|
||||
'name': data['name'],
|
||||
'code': data.get('code'),
|
||||
'type': data.get('type','generic'),
|
||||
'summary': data.get('summary'),
|
||||
'detail': data.get('detail'),
|
||||
'status': data.get('status','draft'),
|
||||
'default_pricing': json.dumps(data.get('default_pricing', {})),
|
||||
'meta': json.dumps(data.get('meta', {}))
|
||||
})
|
||||
return {'id': pid}
|
||||
|
||||
async def update(self, sor, pid, data):
|
||||
await sor.U('product', {
|
||||
'id': pid,
|
||||
'category_id': data.get('category_id'),
|
||||
'name': data.get('name'),
|
||||
'code': data.get('code'),
|
||||
'type': data.get('type'),
|
||||
'summary': data.get('summary'),
|
||||
'detail': data.get('detail'),
|
||||
'status': data.get('status'),
|
||||
'default_pricing': json.dumps(data.get('default_pricing', {})),
|
||||
'meta': json.dumps(data.get('meta', {}))
|
||||
})
|
||||
return {'id': pid}
|
||||
|
||||
async def delete(self, sor, pid):
|
||||
# 软删除为上线安全考虑
|
||||
await sor.U('product', {'id': pid, 'status': 'deleted'})
|
||||
return {'id': pid}
|
||||
|
||||
async def bind_capability(self, sor, pid, cap_id, config=None):
|
||||
bind_id = str(uuid.uuid4())
|
||||
if not config:
|
||||
config = {}
|
||||
# check exists
|
||||
exists = await sor.sqlExe("select 1 from product_capability where product_id=${pid}$ and capability_id=${cid}$ limit 1", {'pid':pid, 'cid':cap_id})
|
||||
if exists:
|
||||
raise Exception("已存在绑定")
|
||||
await sor.C('product_capability', {'id': bind_id, 'product_id': pid, 'capability_id': cap_id, 'config': json.dumps(config or {})})
|
||||
return {'id': bind_id}
|
||||
|
||||
async def unbind_capability(self, sor, bind_id):
|
||||
await sor.D('product_capability', {'id': bind_id})
|
||||
return {'id': bind_id}
|
||||
|
||||
async def change_status(self, sor, pid, status):
|
||||
# status in ['draft','online','offline']
|
||||
if status not in ('draft','online','offline'):
|
||||
raise Exception("状态不合法")
|
||||
await sor.U('product', {'id': pid, 'status': status})
|
||||
return {'id': pid, 'status': status}
|
||||
|
||||
async def get(self, sor, pid):
|
||||
rows = await sor.sqlExe("select * from product where id=${pid}$ limit 1", {'pid':pid})
|
||||
if not rows:
|
||||
return None
|
||||
product = rows[0]
|
||||
# load bindings
|
||||
binds = await sor.sqlExe("select * from product_capability where product_id=${pid}$", {'pid':pid})
|
||||
product['bindings'] = binds
|
||||
# load pricing plans
|
||||
plans = await sor.sqlExe("select * from pricing_plan where product_id=${pid}$ order by is_default desc", {'pid':pid})
|
||||
product['pricing'] = plans
|
||||
return product
|
||||
|
||||
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[tool.poetry]
|
||||
name = "product"
|
||||
version = "0.1.0"
|
||||
description = "Product module for platform - product/category/capability/pricing bindings"
|
||||
authors = ["yumoqing <yumoqing@gmail.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
aiohttp = "*"
|
||||
sqlor = {version="*", optional=true}
|
||||
ahserver = {version="*", optional=true}
|
||||
cryptography = "*"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
0
wwwroot/README.md
Normal file
0
wwwroot/README.md
Normal file
Loading…
x
Reference in New Issue
Block a user