feat: add resource_module field and import sub-categories/products tool
- Add resource_module field to product_category model (stores module name like 'llmage') - Add resource_module appcode with llmage/supplychain entries to init/data.json - Add '添加子产品类和产品导入' toolbar tool to tree CRUD (json + generated index.ui) - Create import_category_products.dspy API endpoint - Add import_categories_and_products() generic import engine to core.py - Register import function on ServerEnv - Update load_path.py with new API endpoint
This commit is contained in:
parent
e8f836ba2d
commit
0f321b8185
@ -64,6 +64,11 @@
|
||||
"id": "billing_mode",
|
||||
"name": "计费模式",
|
||||
"hierarchy_flg": "0"
|
||||
},
|
||||
{
|
||||
"id": "resource_module",
|
||||
"name": "资源模块",
|
||||
"hierarchy_flg": "0"
|
||||
}
|
||||
],
|
||||
"appcodes_kv": [
|
||||
@ -117,7 +122,10 @@
|
||||
{"id": "overflow_mode_2", "parentid": "overflow_mode", "k": "2", "v": "停服"},
|
||||
|
||||
{"id": "billing_mode_1", "parentid": "billing_mode", "k": "1", "v": "配额内"},
|
||||
{"id": "billing_mode_2", "parentid": "billing_mode", "k": "2", "v": "超额按量"}
|
||||
{"id": "billing_mode_2", "parentid": "billing_mode", "k": "2", "v": "超额按量"},
|
||||
|
||||
{"id": "resource_module_llmage", "parentid": "resource_module", "k": "llmage", "v": "大模型管理"},
|
||||
{"id": "resource_module_supplychain", "parentid": "resource_module", "k": "supplychain", "v": "供应链管理"}
|
||||
],
|
||||
"_note_product_category": "产品类别树由每个 reseller (org_id) 自行管理,不在 init/data.json 中预设全局数据。新机构注册时自动创建根类别。"
|
||||
}
|
||||
|
||||
@ -16,6 +16,32 @@
|
||||
"edit_exclouded_fields": ["created_by", "created_at", "updated_at", "org_id", "product_type_title"],
|
||||
"logined_userorgid": "org_id",
|
||||
"browserfields": {},
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "import_products",
|
||||
"label": "添加子产品类和产品导入",
|
||||
"selected_data": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "import_products",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "产品导入结果",
|
||||
"cwidth": 20,
|
||||
"cheight": 10
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../api/import_category_products.dspy')}}",
|
||||
"params": {"category_id": "${id}$"}
|
||||
}
|
||||
}
|
||||
],
|
||||
"subtables": [
|
||||
{
|
||||
"field": "category_id",
|
||||
|
||||
@ -55,6 +55,12 @@
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "resource_module",
|
||||
"title": "资源模块",
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "sort_order",
|
||||
"title": "排序序号",
|
||||
@ -154,6 +160,13 @@
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='product_type'"
|
||||
},
|
||||
{
|
||||
"field": "resource_module",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='resource_module'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -67,6 +67,14 @@ async def set_operator_config(category_id, config_name, config_json, user_id=Non
|
||||
return await manager.set_operator_config(category_id, config_name, config_json, user_id, org_id)
|
||||
|
||||
|
||||
async def import_categories_and_products(org_id, parent_category_id, user_id, import_data):
|
||||
"""Generic import engine for sub-categories and products."""
|
||||
manager = get_manager()
|
||||
return await manager.import_categories_and_products(
|
||||
org_id, parent_category_id, user_id, import_data
|
||||
)
|
||||
|
||||
|
||||
def load_product_management():
|
||||
"""Register all functions with ServerEnv so they can be called from .ui/.dspy files."""
|
||||
env = ServerEnv()
|
||||
@ -78,4 +86,5 @@ def load_product_management():
|
||||
env.get_products_by_category = get_products_by_category
|
||||
env.get_operator_config = get_operator_config
|
||||
env.set_operator_config = set_operator_config
|
||||
env.import_categories_and_products = import_categories_and_products
|
||||
return True
|
||||
|
||||
@ -1090,3 +1090,128 @@ class ProductManager:
|
||||
'overflow_mode': sub.get('overflow_mode'),
|
||||
'overflow_rate': float(sub.get('overflow_rate', 0))
|
||||
}
|
||||
|
||||
# ─── Product Import from Resource Modules ───
|
||||
|
||||
async def import_categories_and_products(self, org_id, parent_category_id,
|
||||
user_id, import_data):
|
||||
"""Generic import engine: create sub-categories and products from structured data.
|
||||
|
||||
import_data format:
|
||||
{
|
||||
'categories': [
|
||||
{'name': '子类别名', 'source_id': 'ext_id', 'parent_source_id': None,
|
||||
'sort_order': 1, 'description': '', 'has_product': '1', 'product_type': 'llm_model'},
|
||||
...
|
||||
],
|
||||
'products': [
|
||||
{'product_code': 'code1', 'product_name': '产品1',
|
||||
'category_source_id': 'ext_id', 'product_type': 'llm_model',
|
||||
'brief_intro': '', 'price': 0, 'currency': 'CNY', 'sort_order': 0},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
source_id is the external module's ID, used to avoid duplicates.
|
||||
Products link to categories via category_source_id matching a category's source_id.
|
||||
"""
|
||||
import time
|
||||
dbname = self._get_dbname()
|
||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
created_cats = 0
|
||||
skipped_cats = 0
|
||||
created_prods = 0
|
||||
skipped_prods = 0
|
||||
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Phase 1: Create sub-categories under parent_category_id
|
||||
# Map source_id -> new category id
|
||||
source_to_id = {}
|
||||
|
||||
for cat in import_data.get('categories', []):
|
||||
source_id = cat.get('source_id', '')
|
||||
# Check if already imported (by name + parent)
|
||||
existing = await sor.sqlExe(
|
||||
"""SELECT id FROM product_category
|
||||
WHERE name = ${name}$ AND parent_id = ${parent_id}$ AND org_id = ${org_id}$""",
|
||||
{'name': cat['name'], 'parent_id': parent_category_id, 'org_id': org_id}
|
||||
)
|
||||
if existing:
|
||||
source_to_id[source_id] = existing[0]['id']
|
||||
skipped_cats += 1
|
||||
continue
|
||||
|
||||
new_id = getID()
|
||||
source_to_id[source_id] = new_id
|
||||
# Determine parent: if parent_source_id is set, use that mapping
|
||||
psrc = cat.get('parent_source_id')
|
||||
actual_parent = source_to_id.get(psrc, parent_category_id) if psrc else parent_category_id
|
||||
|
||||
cat_data = {
|
||||
'id': new_id,
|
||||
'parent_id': actual_parent,
|
||||
'name': cat['name'],
|
||||
'description': cat.get('description', ''),
|
||||
'has_product': cat.get('has_product', '1'),
|
||||
'product_type': cat.get('product_type', ''),
|
||||
'product_type_title': cat.get('product_type_title', ''),
|
||||
'sort_order': str(cat.get('sort_order', 0)),
|
||||
'icon': cat.get('icon', ''),
|
||||
'status': '1',
|
||||
'resource_module': cat.get('resource_module', ''),
|
||||
'org_id': org_id,
|
||||
'created_by': user_id,
|
||||
'created_at': now,
|
||||
'updated_at': now
|
||||
}
|
||||
await sor.C('product_category', cat_data)
|
||||
created_cats += 1
|
||||
|
||||
# Phase 2: Create products under the mapped categories
|
||||
for prod in import_data.get('products', []):
|
||||
cat_source_id = prod.get('category_source_id', '')
|
||||
target_cat_id = source_to_id.get(cat_source_id)
|
||||
if not target_cat_id:
|
||||
skipped_prods += 1
|
||||
continue
|
||||
|
||||
# Check duplicate by product_code + org
|
||||
existing_prod = await sor.sqlExe(
|
||||
"""SELECT id FROM product
|
||||
WHERE product_code = ${code}$ AND org_id = ${org_id}$""",
|
||||
{'code': prod['product_code'], 'org_id': org_id}
|
||||
)
|
||||
if existing_prod:
|
||||
skipped_prods += 1
|
||||
continue
|
||||
|
||||
prod_id = getID()
|
||||
prod_data = {
|
||||
'id': prod_id,
|
||||
'category_id': target_cat_id,
|
||||
'product_code': prod['product_code'],
|
||||
'product_name': prod['product_name'],
|
||||
'product_type': prod.get('product_type', ''),
|
||||
'brief_intro': prod.get('brief_intro', ''),
|
||||
'detail_intro': prod.get('detail_intro', ''),
|
||||
'extra_json': prod.get('extra_json', ''),
|
||||
'enabled_date': prod.get('enabled_date'),
|
||||
'expired_date': prod.get('expired_date'),
|
||||
'status': prod.get('status', '1'),
|
||||
'price_type': prod.get('price_type', '1'),
|
||||
'price': str(prod.get('price', 0)),
|
||||
'currency': prod.get('currency', 'CNY'),
|
||||
'sort_order': str(prod.get('sort_order', 0)),
|
||||
'org_id': org_id,
|
||||
'created_by': user_id,
|
||||
'created_at': now,
|
||||
'updated_at': now
|
||||
}
|
||||
await sor.C('product', prod_data)
|
||||
created_prods += 1
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'导入完成: 新增 {created_cats} 个子类别, {created_prods} 个产品; '
|
||||
f'跳过 {skipped_cats} 个已存在类别, {skipped_prods} 个已存在产品'
|
||||
}
|
||||
|
||||
@ -119,6 +119,9 @@ PATHS_LOGINED = [
|
||||
f"/{MOD}/api/product_resource_supplier_update.dspy",
|
||||
f"/{MOD}/api/product_resource_supplier_delete.dspy",
|
||||
|
||||
# Import
|
||||
f"/{MOD}/api/import_category_products.dspy",
|
||||
|
||||
# Subscriptions
|
||||
f"/{MOD}/product_subscription_list",
|
||||
f"/{MOD}/product_subscription_list/index.ui",
|
||||
|
||||
45
wwwroot/api/import_category_products.dspy
Normal file
45
wwwroot/api/import_category_products.dspy
Normal file
@ -0,0 +1,45 @@
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
category_id = params_kw.get('category_id', '')
|
||||
if not category_id:
|
||||
raise ValueError('请先选择一个产品类别')
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
user_id = await get_user()
|
||||
dbname = get_module_dbname('product_management')
|
||||
|
||||
# Get the selected category's resource_module
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
rows = await sor.sqlExe(
|
||||
"SELECT id, name, resource_module, org_id FROM product_category WHERE id = ${id}$ AND org_id = ${org_id}$",
|
||||
{'id': category_id, 'org_id': org_id}
|
||||
)
|
||||
if not rows:
|
||||
raise ValueError('类别不存在或无权操作')
|
||||
cat = rows[0]
|
||||
|
||||
resource_module = cat.resource_module
|
||||
if not resource_module:
|
||||
raise ValueError('该类别未配置资源模块,请先在类别编辑中设置资源模块')
|
||||
|
||||
# Look up the registered import function by module name
|
||||
env = request._run_ns
|
||||
import_fn_name = f'import_products_from_{resource_module}'
|
||||
import_fn = getattr(env, import_fn_name, None)
|
||||
if import_fn is None:
|
||||
raise ValueError(f'资源模块 "{resource_module}" 未注册产品导入功能')
|
||||
|
||||
# Call the resource module's import function
|
||||
import_result = await import_fn(org_id=org_id, parent_category_id=category_id, user_id=user_id)
|
||||
|
||||
if import_result.get('success'):
|
||||
msg = import_result.get('message', '导入完成')
|
||||
result = {'widgettype': 'Message', 'options': {'title': '导入成功', 'message': msg, 'type': 'success', 'timeout': 5}}
|
||||
else:
|
||||
raise ValueError(import_result.get('error', '导入失败'))
|
||||
|
||||
except Exception as e:
|
||||
result['options'] = {'title': '导入失败', 'message': str(e), 'type': 'error', 'timeout': 5}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
|
||||
|
||||
"toolbar":{"tools":[{"selected_row":true,"name":"product","icon":"{{entire_url('/imgs/product.svg')}}","label":"下属产品"}]},
|
||||
"toolbar":{"tools":[{"selected_row":true,"name":"product","icon":"{{entire_url('/imgs/product.svg')}}","label":"下属产品"},{"name":"import_products","label":"添加子产品类和产品导入","selected_data":true}]},
|
||||
|
||||
|
||||
"editable":{
|
||||
@ -147,6 +147,26 @@
|
||||
"text": "禁用"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "resource_module",
|
||||
"title": "资源模块",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"label": "资源模块",
|
||||
"uitype": "code",
|
||||
"valueField": "resource_module",
|
||||
"textField": "resource_module_text",
|
||||
"params": {
|
||||
"dbname": "{{get_module_dbname('product_management')}}",
|
||||
"table": "appcodes_kv",
|
||||
"tblvalue": "k",
|
||||
"tbltext": "v",
|
||||
"valueField": "resource_module",
|
||||
"textField": "resource_module_text",
|
||||
"cond": "id='resource_module'"
|
||||
},
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
||||
}
|
||||
],
|
||||
|
||||
@ -198,6 +218,21 @@
|
||||
"params": {},
|
||||
"url": "{{entire_url('../product_list')}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "import_products",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "产品导入结果",
|
||||
"cwidth": 20,
|
||||
"cheight": 10
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../api/import_category_products.dspy')}}",
|
||||
"params": {"category_id": "${id}$"}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user