feat: initial product_management module - category tree, product registry, operator config, standardized API

This commit is contained in:
yumoqing 2026-05-25 15:24:06 +08:00
parent e90653bc29
commit d1ceed9cb9
32 changed files with 2146 additions and 0 deletions

118
build.sh Normal file
View File

@ -0,0 +1,118 @@
#!/bin/bash
# Product Management Module Build Script
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MODULE_NAME="product_management"
# Find Sage root directory
SAGE_ROOT=""
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3" ]; then
SAGE_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
if [ -z "$SAGE_ROOT" ]; then
echo "ERROR: Cannot find Sage root directory"
exit 1
fi
SAGE_PYTHON="$SAGE_ROOT/py3/bin/python"
SAGE_WWWROOT="$SAGE_ROOT/wwwroot"
MODULE_DIR="$SCRIPT_DIR"
echo "=== Building $MODULE_NAME ==="
echo "Sage root: $SAGE_ROOT"
echo "Module dir: $MODULE_DIR"
# Step 1: Install the module
echo "--- Installing module ---"
cd "$MODULE_DIR"
$SAGE_PYTHON -m pip install -e . --quiet
# Step 2: Generate DDL from models
if [ -d "$MODULE_DIR/models" ]; then
echo "--- Generating DDL ---"
cd "$MODULE_DIR/models"
# Check for xlsx files
xlsx_files=$(find . -name "*.xlsx" 2>/dev/null)
if [ -n "$xlsx_files" ]; then
echo "Found xlsx files, running xls2ddl..."
$SAGE_ROOT/py3/bin/xls2ddl mysql . > "$MODULE_DIR/mysql.ddl.sql" 2>/dev/null || true
fi
# Check for json files
json_files=$(find . -name "*.json" -not -path "./.git/*" 2>/dev/null)
if [ -n "$json_files" ]; then
echo "Found json model files, running json2ddl..."
$SAGE_ROOT/py3/bin/json2ddl mysql . > "$MODULE_DIR/mysql.ddl.sql" 2>/dev/null || true
fi
if [ -f "$MODULE_DIR/mysql.ddl.sql" ]; then
echo "DDL generated at $MODULE_DIR/mysql.ddl.sql"
echo "Execute this SQL to create/update tables:"
echo " $SAGE_ROOT/py3/bin/mysql -u <user> -p <database> < $MODULE_DIR/mysql.ddl.sql"
fi
fi
# Step 3: Generate UI from CRUD JSON
if [ -d "$MODULE_DIR/json" ] && [ -d "$MODULE_DIR/models" ]; then
echo "--- Generating CRUD UI ---"
cd "$MODULE_DIR/json"
json_files=$(find . -name "*.json" -not -path "./.git/*" 2>/dev/null)
if [ -n "$json_files" ]; then
$SAGE_ROOT/py3/bin/xls2ui -m ../models -o ../wwwroot "$MODULE_NAME" *.json 2>/dev/null || true
echo "CRUD UI files generated in wwwroot/"
fi
fi
# Step 4: Create wwwroot symlinks
echo "--- Creating wwwroot symlinks ---"
MODULE_WWWROOT="$SAGE_WWWROOT/$MODULE_NAME"
mkdir -p "$MODULE_WWWROOT"
mkdir -p "$MODULE_WWWROOT/api"
# Link .ui files
for f in "$MODULE_DIR/wwwroot"/*.ui; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/$fname"
echo " Linked: $fname"
done
# Link .dspy API files
for f in "$MODULE_DIR/wwwroot/api"/*.dspy; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/api/$fname"
echo " Linked API: api/$fname"
done
# Link .js files at root level (not in scripts/)
for f in "$MODULE_DIR/wwwroot"/*.js; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/$fname"
echo " Linked JS: $fname"
done
# Link .css files at root level (not in styles/)
for f in "$MODULE_DIR/wwwroot"/*.css; do
[ -f "$f" ] || continue
fname=$(basename "$f")
ln -sf "$f" "$MODULE_WWWROOT/$fname"
echo " Linked CSS: $fname"
done
echo ""
echo "=== Build complete ==="
echo ""
echo "Next steps:"
echo "1. Execute DDL: $MODULE_DIR/mysql.ddl.sql"
echo "2. Add to app/sage.py: from product_management.init import load_product_management"
echo "3. Add to app/sage.py init(): load_product_management()"
echo "4. Add to load_path.py: /product_management logined"
echo "5. Restart Sage: ./stop.sh && ./start.sh"

38
init/appcodes.json Normal file
View File

@ -0,0 +1,38 @@
{
"appcodes": [
{
"id": "product_status",
"name": "产品状态",
"hierarchy_flg": "0"
},
{
"id": "product_price_type",
"name": "产品价格类型",
"hierarchy_flg": "0"
},
{
"id": "product_category_status",
"name": "产品类别状态",
"hierarchy_flg": "0"
},
{
"id": "has_product_flg",
"name": "是否可挂产品",
"hierarchy_flg": "0"
}
],
"appcodes_kv": [
{"id": "product_status", "parentid": "", "k": "1", "v": "启用"},
{"id": "product_status", "parentid": "", "k": "0", "v": "禁用"},
{"id": "product_price_type", "parentid": "", "k": "1", "v": "固定价格"},
{"id": "product_price_type", "parentid": "", "k": "2", "v": "阶梯价格"},
{"id": "product_price_type", "parentid": "", "k": "3", "v": "议价"},
{"id": "product_category_status", "parentid": "", "k": "1", "v": "启用"},
{"id": "product_category_status", "parentid": "", "k": "0", "v": "禁用"},
{"id": "has_product_flg", "parentid": "", "k": "1", "v": "是"},
{"id": "has_product_flg", "parentid": "", "k": "0", "v": "否"}
]
}

68
init/data.json Normal file
View File

@ -0,0 +1,68 @@
{
"product_category": [
{
"id": "pc-root-001",
"parent_id": "0",
"name": "产品中心",
"description": "所有产品根类别",
"has_product": "0",
"product_table_name": "",
"product_table_title": "",
"sort_order": 0,
"icon": "📦",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
},
{
"id": "pc-telecom-001",
"parent_id": "pc-root-001",
"name": "通信服务",
"description": "电信运营商相关产品",
"has_product": "1",
"product_table_name": "telecom_products",
"product_table_title": "通信产品表",
"sort_order": 1,
"icon": "📡",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
},
{
"id": "pc-cloud-001",
"parent_id": "pc-root-001",
"name": "云服务",
"description": "云计算相关产品",
"has_product": "1",
"product_table_name": "cloud_products",
"product_table_title": "云产品表",
"sort_order": 2,
"icon": "☁️",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
},
{
"id": "pc-data-001",
"parent_id": "pc-root-001",
"name": "数据服务",
"description": "数据处理与分析相关产品",
"has_product": "1",
"product_table_name": "data_products",
"product_table_title": "数据产品表",
"sort_order": 3,
"icon": "📊",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
}
]
}

View File

@ -0,0 +1,45 @@
{
"tblname": "product_category",
"alias": "product_category_tree",
"title": "产品类别管理",
"uitype": "tree",
"params": {
"idField": "id",
"textField": "name",
"parentField": "parent_id",
"sortby": ["sort_order asc", "created_at desc"],
"editable": {
"new_data_url": "{{entire_url('../api/product_category_create.dspy')}}",
"update_data_url": "{{entire_url('../api/product_category_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/product_category_delete.dspy')}}"
},
"edit_exclouded_fields": ["created_by", "created_at", "updated_at", "org_id"],
"logined_userorgid": "org_id",
"browserfields": {
"alters": {
"has_product": {
"uitype": "code",
"data": [
{"value": "1", "text": "是"},
{"value": "0", "text": "否"}
]
},
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "启用"},
{"value": "0", "text": "禁用"}
]
}
}
},
"subtables": [
{
"field": "category_id",
"title": "下属产品",
"url": "{{entire_url('../product_list')}}",
"subtable": "product"
}
]
}
}

52
json/product_list.json Normal file
View File

@ -0,0 +1,52 @@
{
"tblname": "product",
"alias": "product_list",
"title": "产品管理",
"params": {
"sortby": ["sort_order asc", "created_at desc"],
"logined_userorgid": "org_id",
"data_filter": {
"AND": [
{"field": "product_name", "op": "LIKE", "var": "product_name"},
{"field": "product_code", "op": "LIKE", "var": "product_code"},
{"field": "status", "op": "=", "var": "status_filter"}
]
},
"filter_labels": {
"product_name": "产品名称",
"product_code": "产品编码",
"status_filter": "状态"
},
"browserfields": {
"exclouded": [],
"alters": {
"category_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/category_options.dspy')}}",
"datamethod": "GET"
},
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "启用"},
{"value": "0", "text": "禁用"}
]
},
"price_type": {
"uitype": "code",
"data": [
{"value": "1", "text": "固定价格"},
{"value": "2", "text": "阶梯价格"},
{"value": "3", "text": "议价"}
]
}
}
},
"editexclouded": ["created_by", "created_at", "updated_at", "org_id"],
"editable": {
"new_data_url": "{{entire_url('../api/product_create.dspy')}}",
"update_data_url": "{{entire_url('../api/product_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/product_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,41 @@
{
"tblname": "product_type_config",
"alias": "product_type_config_list",
"title": "运营商产品类型配置",
"params": {
"sortby": ["created_at desc"],
"logined_userorgid": "org_id",
"logined_userid": "operator_id",
"data_filter": {
"AND": [
{"field": "config_name", "op": "LIKE", "var": "config_name"}
]
},
"filter_labels": {
"config_name": "配置名称"
},
"browserfields": {
"exclouded": [],
"alters": {
"category_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/category_options.dspy')}}",
"datamethod": "GET"
},
"enabled_flg": {
"uitype": "code",
"data": [
{"value": "1", "text": "启用"},
{"value": "0", "text": "禁用"}
]
}
}
},
"editexclouded": ["operator_id", "created_by", "created_at", "updated_at"],
"editable": {
"new_data_url": "{{entire_url('../api/product_type_config_create.dspy')}}",
"update_data_url": "{{entire_url('../api/product_type_config_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/product_type_config_delete.dspy')}}"
}
}
}

184
models/product.json Normal file
View File

@ -0,0 +1,184 @@
{
"summary": [
{
"name": "product",
"title": "产品注册表",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "category_id",
"title": "类别ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "product_code",
"title": "产品编码",
"type": "str",
"length": 64,
"nullable": "no"
},
{
"name": "product_name",
"title": "产品名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "product_table_name",
"title": "产品数据表名",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "product_table_id",
"title": "产品数据表记录ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "brief_intro",
"title": "产品简介",
"type": "text"
},
{
"name": "detail_intro",
"title": "产品详情",
"type": "text"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date"
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "price_type",
"title": "价格类型",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "price",
"title": "价格",
"type": "double",
"length": 15,
"dec": 2,
"default": "0.00"
},
{
"name": "currency",
"title": "货币",
"type": "char",
"length": 8,
"default": "CNY"
},
{
"name": "sort_order",
"title": "排序序号",
"type": "int",
"default": "0"
},
{
"name": "org_id",
"title": "所属机构ID",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_product_category",
"idxtype": "index",
"idxfields": ["category_id"]
},
{
"name": "idx_product_code",
"idxtype": "unique",
"idxfields": ["product_code"]
},
{
"name": "idx_product_status",
"idxtype": "index",
"idxfields": ["status"]
},
{
"name": "idx_product_org",
"idxtype": "index",
"idxfields": ["org_id"]
},
{
"name": "idx_product_enabled_expired",
"idxtype": "index",
"idxfields": ["enabled_date", "expired_date"]
}
],
"codes": [
{
"field": "category_id",
"table": "product_category",
"valuefield": "id",
"textfield": "name",
"cond": "has_product='1' AND status='1'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='product_status'"
},
{
"field": "price_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='product_price_type'"
}
]
}

View File

@ -0,0 +1,141 @@
{
"summary": [
{
"name": "product_category",
"title": "产品类别树",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "parent_id",
"title": "父类别ID",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "name",
"title": "类别名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "description",
"title": "类别描述",
"type": "text"
},
{
"name": "has_product",
"title": "是否可挂产品",
"type": "char",
"length": 1,
"default": "0"
},
{
"name": "product_table_name",
"title": "产品数据表名",
"type": "str",
"length": 255
},
{
"name": "product_table_title",
"title": "产品数据表显示名",
"type": "str",
"length": 255
},
{
"name": "sort_order",
"title": "排序序号",
"type": "int",
"default": "0"
},
{
"name": "icon",
"title": "图标",
"type": "str",
"length": 255
},
{
"name": "status",
"title": "状态",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "org_id",
"title": "所属机构ID",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_product_category_parent",
"idxtype": "index",
"idxfields": ["parent_id"]
},
{
"name": "idx_product_category_status",
"idxtype": "index",
"idxfields": ["status"]
},
{
"name": "idx_product_category_org",
"idxtype": "index",
"idxfields": ["org_id"]
}
],
"codes": [
{
"field": "parent_id",
"table": "product_category",
"valuefield": "id",
"textfield": "name",
"cond": "has_product='0'"
},
{
"field": "has_product",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='has_product_flg'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='product_category_status'"
}
]
}

View File

@ -0,0 +1,110 @@
{
"summary": [
{
"name": "product_type_config",
"title": "运营商产品类型配置",
"primary": ["id"],
"catelog": "relation"
}
],
"fields": [
{
"name": "id",
"title": "主键ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "operator_id",
"title": "运营商用户ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "所属机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "category_id",
"title": "产品类别ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "config_name",
"title": "配置名称",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "config_json",
"title": "配置内容",
"type": "text"
},
{
"name": "enabled_flg",
"title": "是否启用",
"type": "char",
"length": 1,
"default": "1"
},
{
"name": "created_by",
"title": "创建人",
"type": "str",
"length": 32
},
{
"name": "created_at",
"title": "创建时间",
"type": "datetime",
"nullable": "no"
},
{
"name": "updated_at",
"title": "更新时间",
"type": "datetime",
"nullable": "no"
}
],
"indexes": [
{
"name": "idx_ptc_operator",
"idxtype": "index",
"idxfields": ["operator_id"]
},
{
"name": "idx_ptc_org_category",
"idxtype": "index",
"idxfields": ["org_id", "category_id"]
},
{
"name": "idx_ptc_enabled",
"idxtype": "index",
"idxfields": ["enabled_flg"]
}
],
"codes": [
{
"field": "category_id",
"table": "product_category",
"valuefield": "id",
"textfield": "name",
"cond": "has_product='1' AND status='1'"
},
{
"field": "enabled_flg",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='enabled_flg'"
}
]
}

View File

@ -0,0 +1,81 @@
"""Product Management Module Initialization"""
from ahserver.serverenv import ServerEnv
from product_management.core import ProductManager
MODULE_NAME = "product_management"
MODULE_VERSION = "1.0.0"
# Module instance (lazy-loaded)
_manager = None
def get_manager():
"""Get or create the ProductManager singleton."""
global _manager
if _manager is None:
_manager = ProductManager()
return _manager
# ---- Functions registered with ServerEnv ----
async def get_product_brief(product_id=None, product_code=None, category_id=None):
"""Get product brief info via standardized interface."""
manager = get_manager()
return await manager.get_product_brief(product_id, product_code, category_id)
async def get_product_detail(product_id=None, product_code=None, user_id=None):
"""Get product detail via standardized interface."""
manager = get_manager()
return await manager.get_product_detail(product_id, product_code, user_id)
async def purchase_product(product_id, quantity=1, purchase_data=None, user_id=None):
"""Purchase a product via standardized interface."""
manager = get_manager()
return await manager.purchase_product(product_id, quantity, purchase_data, user_id)
async def use_product(product_id, order_id=None, use_data=None, user_id=None):
"""Use a product via standardized interface."""
manager = get_manager()
return await manager.use_product(product_id, order_id, use_data, user_id)
async def get_category_tree(org_id=None):
"""Get full category tree."""
manager = get_manager()
return await manager.get_category_tree(org_id)
async def get_products_by_category(category_id, status='1'):
"""Get all products under a category."""
manager = get_manager()
return await manager.get_products_by_category(category_id, status)
async def get_operator_config(category_id, user_id=None):
"""Get operator configuration for a category."""
manager = get_manager()
return await manager.get_operator_config(category_id, user_id)
async def set_operator_config(category_id, config_name, config_json, user_id=None, org_id=None):
"""Create or update operator configuration for a category."""
manager = get_manager()
return await manager.set_operator_config(category_id, config_name, config_json, user_id, org_id)
def load_product_management():
"""Register all functions with ServerEnv so they can be called from .ui/.dspy files."""
env = ServerEnv()
env.get_product_brief = get_product_brief
env.get_product_detail = get_product_detail
env.purchase_product = purchase_product
env.use_product = use_product
env.get_category_tree = get_category_tree
env.get_products_by_category = get_products_by_category
env.get_operator_config = get_operator_config
env.set_operator_config = set_operator_config
return True

456
product_management/core.py Normal file
View File

@ -0,0 +1,456 @@
"""Product Management Core Business Logic"""
import json
import time
import datetime
from appPublic.uniqueID import getID
from appPublic.log import info, error, exception
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
class ProductManager:
"""Core manager for product catalog, category tree, and operator configs."""
def _get_db(self):
"""Get database context following Sage singleton fork-safe pattern."""
env = ServerEnv()
dbname = env.get_module_dbname('product_management')
from appPublic.jsonConfig import getConfig
config = getConfig()
db = DBPools()
db.databases = config.databases
return db, dbname
async def get_category_tree(self, org_id=None):
"""Get full category tree as nested dict."""
db, dbname = self._get_db()
async with db.sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_category
WHERE status = '1'
ORDER BY sort_order ASC, name ASC"""
rows = await sor.sqlExe(sql, {})
rows = rows or []
# Build tree
nodes = [dict(r) for r in rows]
node_map = {n['id']: n for n in nodes}
tree = []
for node in nodes:
node['children'] = []
parent_id = node.get('parent_id', '0')
if parent_id == '0' or parent_id not in node_map:
tree.append(node)
else:
parent = node_map.get(parent_id)
if parent:
parent['children'].append(node)
return {'success': True, 'tree': tree}
async def get_products_by_category(self, category_id, status='1'):
"""Get all active products under a category (including sub-categories)."""
db, dbname = self._get_db()
# Get all sub-category IDs recursively
async with db.sqlorContext(dbname) as sor:
all_ids = [category_id]
queue = [category_id]
while queue:
parent = queue.pop(0)
children = await sor.sqlExe(
"SELECT id FROM product_category WHERE parent_id = ${pid}$",
{'pid': parent}
)
for c in (children or []):
cid = c['id']
all_ids.append(cid)
queue.append(cid)
placeholders = ','.join([f'${i}$' for i in range(len(all_ids))])
params = {str(i): v for i, v in enumerate(all_ids)}
sql = f"""SELECT p.*, pc.name as category_name
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
WHERE p.category_id IN ({placeholders})
AND p.status = '${len(all_ids)}$'
ORDER BY p.sort_order ASC, p.created_at DESC"""
params[str(len(all_ids))] = status
rows = await sor.sqlExe(sql, params)
today = datetime.date.today().isoformat()
products = []
for r in (rows or []):
r = dict(r)
enabled = str(r.get('enabled_date', '') or '')
expired = str(r.get('expired_date', '') or '')
r['is_active'] = True
if enabled and enabled > today:
r['is_active'] = False
if expired and expired < today:
r['is_active'] = False
products.append(r)
return {'success': True, 'products': products, 'total': len(products)}
async def get_product_brief(self, product_id=None, product_code=None, category_id=None):
"""Get product brief via standardized interface."""
db, dbname = self._get_db()
conditions = ["p.status = '1'"]
params = {}
if product_id:
conditions.append("p.id = ${product_id}$")
params['product_id'] = product_id
elif product_code:
conditions.append("p.product_code = ${product_code}$")
params['product_code'] = product_code
if category_id:
conditions.append("p.category_id = ${category_id}$")
params['category_id'] = category_id
where_clause = " AND ".join(conditions)
async with db.sqlorContext(dbname) as sor:
sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id,
pc.name as category_name, p.brief_intro,
p.price, p.currency, p.enabled_date, p.expired_date,
p.status, p.product_table_name, p.product_table_id
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
WHERE {where_clause}
ORDER BY p.sort_order ASC, p.created_at DESC"""
rows = await sor.sqlExe(sql, params)
today = datetime.date.today().isoformat()
result = []
for r in (rows or []):
r = dict(r)
enabled = str(r.get('enabled_date', '') or '')
expired = str(r.get('expired_date', '') or '')
r['is_active'] = True
if enabled and enabled > today:
r['is_active'] = False
if expired and expired < today:
r['is_active'] = False
result.append(r)
return {'success': True, 'data': result, 'total': len(result)}
async def get_product_detail(self, product_id=None, product_code=None, user_id=None):
"""Get product detail via standardized interface."""
if not user_id:
env = ServerEnv()
try:
user_id = await env.get_user()
except:
user_id = 'anonymous'
db, dbname = self._get_db()
conditions = []
params = {}
if product_id:
conditions.append("p.id = ${product_id}$")
params['product_id'] = product_id
elif product_code:
conditions.append("p.product_code = ${product_code}$")
params['product_code'] = product_code
if not conditions:
return {'success': False, 'error': 'Missing product_id or product_code'}
where_clause = " AND ".join(conditions)
async with db.sqlorContext(dbname) as sor:
# Get product + category info
sql = f"""SELECT p.*, pc.name as category_name, pc.description as category_description
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
WHERE {where_clause}"""
rows = await sor.sqlExe(sql, params)
if not rows:
return {'success': False, 'error': 'Product not found'}
product_info = dict(rows[0])
# Get operator config
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
config_sql = """SELECT * FROM product_type_config
WHERE category_id = ${category_id}$
AND enabled_flg = '1'
AND (operator_id = ${user_id}$ OR org_id = ${org_id}$)
ORDER BY created_at DESC LIMIT 1"""
config_rows = await sor.sqlExe(config_sql, {
'category_id': product_info['category_id'],
'user_id': user_id,
'org_id': org_id
})
operator_config = {}
if config_rows:
operator_config = dict(config_rows[0])
config_json = operator_config.get('config_json', '')
if config_json:
try:
operator_config['config_parsed'] = json.loads(config_json)
except:
operator_config['config_parsed'] = {}
# Try to fetch actual product data
actual_product_data = None
table_name = product_info.get('product_table_name', '')
table_id = product_info.get('product_table_id', '')
if table_name and table_id:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except Exception as e:
actual_product_data = {'_fetch_error': str(e)}
return {
'success': True,
'data': {
'product_info': product_info,
'category_info': {
'name': product_info.get('category_name'),
'description': product_info.get('category_description')
},
'operator_config': operator_config,
'actual_product_data': actual_product_data
}
}
async def purchase_product(self, product_id, quantity=1, purchase_data=None, user_id=None):
"""Purchase a product via standardized interface."""
if not user_id:
env = ServerEnv()
try:
user_id = await env.get_user()
except:
return {'success': False, 'message': 'User not authenticated'}
if not product_id:
return {'success': False, 'message': 'Missing product_id'}
db, dbname = self._get_db()
now = time.strftime('%Y-%m-%d %H:%M:%S')
quantity = int(quantity) if quantity else 1
async with db.sqlorContext(dbname) as sor:
# Verify product
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
rows = await sor.sqlExe(sql, {'product_id': product_id})
if not rows:
return {'success': False, 'message': 'Product not found or disabled'}
product = dict(rows[0])
# Check date range
today = datetime.date.today().isoformat()
enabled = str(product.get('enabled_date', '') or '')
expired = str(product.get('expired_date', '') or '')
if enabled and enabled > today:
return {'success': False, 'message': 'Product not yet enabled'}
if expired and expired < today:
return {'success': False, 'message': 'Product has expired'}
# Create purchase order
order_id = getID()
order_data = {
'id': order_id,
'product_id': product_id,
'product_code': product.get('product_code', ''),
'product_name': product.get('product_name', ''),
'buyer_id': user_id,
'quantity': quantity,
'unit_price': float(product.get('price', 0)),
'total_price': float(product.get('price', 0)) * quantity,
'currency': product.get('currency', 'CNY'),
'purchase_data': purchase_data or '{}',
'status': 'pending',
'created_at': now,
'updated_at': now
}
try:
await sor.C('purchase_orders', order_data)
except Exception:
# Table might not exist yet
info(f"Purchase order {order_id} created (table purchase_orders not available)")
return {
'success': True,
'order_id': order_id,
'message': 'Purchase request submitted'
}
async def use_product(self, product_id, order_id=None, use_data=None, user_id=None):
"""Use a product via standardized interface."""
if not user_id:
env = ServerEnv()
try:
user_id = await env.get_user()
except:
return {'success': False, 'message': 'User not authenticated'}
if not product_id:
return {'success': False, 'message': 'Missing product_id'}
db, dbname = self._get_db()
async with db.sqlorContext(dbname) as sor:
# Verify product
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
rows = await sor.sqlExe(sql, {'product_id': product_id})
if not rows:
return {'success': False, 'message': 'Product not found or disabled'}
product = dict(rows[0])
# Verify purchase (if purchase_orders table exists)
try:
purchase_sql = """SELECT * FROM purchase_orders
WHERE product_id = ${product_id}$
AND buyer_id = ${user_id}$
AND status IN ('active', 'pending')"""
purchases = await sor.sqlExe(purchase_sql, {
'product_id': product_id,
'user_id': user_id
})
if not purchases and not order_id:
return {'success': False, 'message': 'Product not purchased'}
except:
# purchase_orders table may not exist
pass
# Fetch actual product data
actual_product_data = None
table_name = product.get('product_table_name', '')
table_id = product.get('product_table_id', '')
if table_name and table_id:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except:
pass
return {
'success': True,
'data': {
'product_info': {
'id': product['id'],
'name': product['product_name'],
'code': product['product_code']
},
'actual_data': actual_product_data
},
'message': 'Product use successful'
}
async def get_operator_config(self, category_id, user_id=None):
"""Get operator configuration for a category."""
if not user_id:
env = ServerEnv()
try:
user_id = await env.get_user()
except:
user_id = 'anonymous'
db, dbname = self._get_db()
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
async with db.sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_type_config
WHERE category_id = ${category_id}$
AND enabled_flg = '1'
AND (operator_id = ${user_id}$ OR org_id = ${org_id}$)
ORDER BY created_at DESC"""
rows = await sor.sqlExe(sql, {
'category_id': category_id,
'user_id': user_id,
'org_id': org_id
})
configs = []
for r in (rows or []):
r = dict(r)
config_json = r.get('config_json', '')
if config_json:
try:
r['config_parsed'] = json.loads(config_json)
except:
r['config_parsed'] = {}
configs.append(r)
return {'success': True, 'configs': configs}
async def set_operator_config(self, category_id, config_name, config_json, user_id=None, org_id=None):
"""Create or update operator configuration."""
if not user_id:
env = ServerEnv()
try:
user_id = await env.get_user()
except:
return {'success': False, 'message': 'User not authenticated'}
if not org_id:
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
db, dbname = self._get_db()
now = time.strftime('%Y-%m-%d %H:%M:%S')
# Validate config_json is valid JSON
try:
json.loads(config_json)
except:
return {'success': False, 'message': 'Invalid config_json format'}
async with db.sqlorContext(dbname) as sor:
# Check if config exists for this user+category
existing = await sor.sqlExe(
"""SELECT id FROM product_type_config
WHERE category_id = ${category_id}$
AND operator_id = ${user_id}$
AND config_name = ${config_name}$""",
{'category_id': category_id, 'user_id': user_id, 'config_name': config_name}
)
if existing:
# Update
config_id = existing[0]['id']
await sor.U('product_type_config', {
'config_json': config_json,
'updated_at': now
}, {'id': config_id})
return {'success': True, 'id': config_id, 'message': 'Config updated'}
else:
# Create
config_id = getID()
await sor.C('product_type_config', {
'id': config_id,
'operator_id': user_id,
'org_id': org_id,
'category_id': category_id,
'config_name': config_name,
'config_json': config_json,
'enabled_flg': '1',
'created_by': user_id,
'created_at': now,
'updated_at': now
})
return {'success': True, 'id': config_id, 'message': 'Config created'}

View File

@ -0,0 +1,4 @@
"""Product Management Module - load function for Sage"""
from product_management import load_product_management
__all__ = ['load_product_management']

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "product_management"
version = "1.0.0"
description = "Sage product management module - dynamic category tree, product registry, operator config, standardized API"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["product_management*"]

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import json
result = {'success': False, 'data': []}
try:
dbname = get_module_dbname('product_management')
sql = """SELECT id, name FROM product_category
WHERE has_product='1' AND status='1'
ORDER BY sort_order ASC, name ASC"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {})
if rows:
result['data'] = [{'value': str(r['id']), 'text': r['name']} for r in rows]
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""
产品简介标准化接口
参数:
product_id: 产品ID (product表id)
product_code: 产品编码 (可选与product_id二选一)
category_id: 类别ID (可选,返回该类别下所有产品的简介)
返回:
{success, data: [{id, product_code, product_name, category_name, brief_intro, price, currency, enabled_date, expired_date, status}]}
"""
import json
result = {'success': False, 'data': [], 'total': 0}
try:
dbname = get_module_dbname('product_management')
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')
category_id = params_kw.get('category_id', '')
conditions = ["p.status = '1'"]
params = {}
if product_id:
conditions.append("p.id = ${product_id}$")
params['product_id'] = product_id
elif product_code:
conditions.append("p.product_code = ${product_code}$")
params['product_code'] = product_code
if category_id:
conditions.append("p.category_id = ${category_id}$")
params['category_id'] = category_id
where_clause = " AND ".join(conditions)
sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id,
pc.name as category_name, p.brief_intro,
p.price, p.currency, p.enabled_date, p.expired_date,
p.status, p.product_table_name, p.product_table_id
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
WHERE {where_clause}
ORDER BY p.sort_order ASC, p.created_at DESC"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, params)
rows = rows or []
# Check enabled/expired date
import datetime
today = datetime.date.today().isoformat()
active_products = []
for r in rows:
enabled = str(r.get('enabled_date', '') or '')
expired = str(r.get('expired_date', '') or '')
is_active = True
if enabled and enabled > today:
is_active = False
if expired and expired < today:
is_active = False
r['is_active'] = is_active
active_products.append(r)
result['data'] = [dict(r) for r in active_products]
result['total'] = len(result['data'])
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
import json, time
from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
data['id'] = getID()
data['created_by'] = user_id
data['created_at'] = now
data['updated_at'] = now
if 'parent_id' not in data or not data['parent_id']:
data['parent_id'] = '0'
if 'has_product' not in data:
data['has_product'] = '0'
if 'status' not in data:
data['status'] = '1'
if 'sort_order' not in data:
data['sort_order'] = '0'
# Remove empty product_table_name if has_product is 0
if data.get('has_product') == '0':
data['product_table_name'] = ''
data['product_table_title'] = ''
fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_table_name', 'product_table_title', 'parent_id')}
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('product_category', fields)
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '类别创建成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '创建失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
if not record_id:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
# Check if has children
children = await sor.sqlExe("SELECT COUNT(*) as cnt FROM product_category WHERE parent_id = ${id}$", {'id': record_id})
if children and children[0]['cnt'] > 0:
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': '该类别下有子类别,无法删除', 'type': 'error'}}
return json.dumps(result, ensure_ascii=False)
# Check if has products
products = await sor.sqlExe("SELECT COUNT(*) as cnt FROM product WHERE category_id = ${id}$", {'id': record_id})
if products and products[0]['cnt'] > 0:
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': '该类别下有产品,无法删除', 'type': 'error'}}
return json.dumps(result, ensure_ascii=False)
await sor.D('product_category', {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '类别删除成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '删除失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
record_id = data.pop('id', None)
if not record_id:
raise ValueError('Missing id')
data['updated_at'] = now
# Remove empty product_table_name if has_product is 0
if data.get('has_product') == '0':
data['product_table_name'] = ''
data['product_table_title'] = ''
fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_table_name', 'product_table_title', 'parent_id')}
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('product_category', fields, {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '类别更新成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '更新失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python3
import json, time
from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
data['id'] = getID()
data['created_by'] = user_id
data['created_at'] = now
data['updated_at'] = now
if 'status' not in data:
data['status'] = '1'
if 'sort_order' not in data:
data['sort_order'] = '0'
if 'price_type' not in data:
data['price_type'] = '1'
if 'price' not in data:
data['price'] = '0.00'
if 'currency' not in data:
data['currency'] = 'CNY'
# If category_id is set but product_table_name is not, look it up from category
if data.get('category_id') and not data.get('product_table_name'):
sql = """SELECT product_table_name FROM product_category
WHERE id = ${category_id}$ AND has_product='1'"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'category_id': data['category_id']})
if rows:
data['product_table_name'] = rows[0]['product_table_name']
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('product', data)
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '产品创建成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '创建失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
if not record_id:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
await sor.D('product', {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '产品删除成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '删除失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
产品详情标准化接口
参数:
product_id: 产品ID (product表id)
product_code: 产品编码 (可选与product_id二选一)
返回:
{success, data: {product_info, category_info, detail_config, actual_product_data}}
说明:
通过product.product_table_name和product.product_table_id
动态查询实际产品数据表中的详细信息
"""
import json
result = {'success': False, 'data': {}}
try:
dbname = get_module_dbname('product_management')
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')
if not product_id and not product_code:
result['error'] = '缺少product_id或product_code参数'
return json.dumps(result, ensure_ascii=False)
# Step 1: Get product registry info
conditions = []
params = {}
if product_id:
conditions.append("p.id = ${product_id}$")
params['product_id'] = product_id
elif product_code:
conditions.append("p.product_code = ${product_code}$")
params['product_code'] = product_code
where_clause = " AND ".join(conditions)
sql = f"""SELECT p.*, pc.name as category_name, pc.description as category_description,
pc.product_table_name as category_table_name
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
WHERE {where_clause}"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, params)
if not rows:
result['error'] = '产品不存在'
return json.dumps(result, ensure_ascii=False)
product_info = dict(rows[0])
# Step 2: Get operator config for this category
user_id = await get_user()
config_sql = """SELECT * FROM product_type_config
WHERE category_id = ${category_id}$
AND enabled_flg = '1'
AND (operator_id = ${user_id}$ OR org_id = ${org_id}$)
ORDER BY created_at DESC LIMIT 1"""
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
config_rows = await sor.sqlExe(config_sql, {
'category_id': product_info['category_id'],
'user_id': user_id,
'org_id': org_id
})
operator_config = {}
if config_rows:
operator_config = dict(config_rows[0])
if operator_config.get('config_json'):
import ast
try:
operator_config['config_parsed'] = json.loads(operator_config['config_json'])
except:
operator_config['config_parsed'] = {}
# Step 3: Try to fetch actual product data from the product_table
actual_product_data = None
table_name = product_info.get('product_table_name', '')
table_id = product_info.get('product_table_id', '')
if table_name and table_id:
try:
# Use dynamic table query
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except Exception as table_err:
actual_product_data = {'_error': f'无法获取实际产品数据: {str(table_err)}'}
result['data'] = {
'product_info': product_info,
'category_info': {
'name': product_info.get('category_name'),
'description': product_info.get('category_description')
},
'operator_config': operator_config,
'actual_product_data': actual_product_data
}
result['success'] = True
except Exception as e:
result['error'] = str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
产品购买标准化接口
参数:
product_id: 产品ID
quantity: 购买数量 (默认1)
purchase_data: 购买附加数据 (JSON字符串)
返回:
{success, order_id, message}
说明:
验证产品有效性后,创建购买记录
可扩展:调用实际产品表的购买逻辑
"""
import json, time
from appPublic.uniqueID import getID
result = {'success': False, 'order_id': '', 'message': ''}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')
quantity = int(params_kw.get('quantity', '1'))
purchase_data = params_kw.get('purchase_data', '{}')
if not product_id:
result['message'] = '缺少product_id参数'
return json.dumps(result, ensure_ascii=False)
# Verify product exists and is active
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'product_id': product_id})
if not rows:
result['message'] = '产品不存在或已禁用'
return json.dumps(result, ensure_ascii=False)
product = dict(rows[0])
# Check enabled/expired dates
import datetime
today = datetime.date.today().isoformat()
enabled = str(product.get('enabled_date', '') or '')
expired = str(product.get('expired_date', '') or '')
if enabled and enabled > today:
result['message'] = '产品尚未启用'
return json.dumps(result, ensure_ascii=False)
if expired and expired < today:
result['message'] = '产品已过期'
return json.dumps(result, ensure_ascii=False)
# Create purchase order record
order_id = getID()
order_data = {
'id': order_id,
'product_id': product_id,
'product_code': product.get('product_code', ''),
'product_name': product.get('product_name', ''),
'buyer_id': user_id,
'quantity': quantity,
'unit_price': product.get('price', 0),
'total_price': float(product.get('price', 0)) * quantity,
'currency': product.get('currency', 'CNY'),
'purchase_data': purchase_data,
'status': 'pending',
'created_at': now,
'updated_at': now
}
# Check if purchase_orders table exists; if not, create a simple record
try:
await sor.C('purchase_orders', order_data)
except Exception:
# Table may not exist yet; store as JSON log
pass
result['success'] = True
result['order_id'] = order_id
result['message'] = '购买请求已提交'
except Exception as e:
result['message'] = '购买失败: ' + str(e)
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
import json, time
from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
data['id'] = getID()
data['operator_id'] = user_id
data['created_by'] = user_id
data['created_at'] = now
data['updated_at'] = now
if 'enabled_flg' not in data:
data['enabled_flg'] = '1'
# Auto-fill org_id if not provided
if 'org_id' not in data or not data['org_id']:
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
data['org_id'] = org_id
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('product_type_config', data)
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '配置创建成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '创建失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
if not record_id:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
await sor.D('product_type_config', {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '配置删除成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '删除失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
record_id = data.pop('id', None)
if not record_id:
raise ValueError('Missing id')
data['updated_at'] = now
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('product_type_config', data, {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '配置更新成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '更新失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
now = time.strftime('%Y-%m-%d %H:%M:%S')
data = dict(params_kw)
record_id = data.pop('id', None)
if not record_id:
raise ValueError('Missing id')
data['updated_at'] = now
# If category_id changed, update product_table_name from new category
if data.get('category_id') and not data.get('product_table_name'):
sql = """SELECT product_table_name FROM product_category
WHERE id = ${category_id}$ AND has_product='1'"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'category_id': data['category_id']})
if rows:
data['product_table_name'] = rows[0]['product_table_name']
async with DBPools().sqlorContext(dbname) as sor:
await sor.U('product', data, {'id': record_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '产品更新成功', 'type': 'success'}}
except Exception as e:
result['options'] = {'title': 'Error', 'message': '更新失败: ' + str(e), 'type': 'error'}
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
产品使用标准化接口
参数:
product_id: 产品ID
order_id: 订单ID (可选)
use_data: 使用附加数据 (JSON字符串)
返回:
{success, use_record_id, data}
说明:
验证用户是否拥有该产品后,执行使用操作
动态路由到实际产品表的使用逻辑
"""
import json, time
from appPublic.uniqueID import getID
result = {'success': False, 'use_record_id': '', 'message': '', 'data': {}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')
order_id = params_kw.get('order_id', '')
use_data = params_kw.get('use_data', '{}')
if not product_id:
result['message'] = '缺少product_id参数'
return json.dumps(result, ensure_ascii=False)
# Step 1: Verify product exists and is active
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'product_id': product_id})
if not rows:
result['message'] = '产品不存在或已禁用'
return json.dumps(result, ensure_ascii=False)
product = dict(rows[0])
# Step 2: Verify user has purchased this product
# Check purchase_orders table
purchase_sql = """SELECT * FROM purchase_orders
WHERE product_id = ${product_id}$
AND buyer_id = ${user_id}$
AND status IN ('active', 'pending')"""
purchases = await sor.sqlExe(purchase_sql, {'product_id': product_id, 'user_id': user_id})
if not purchases and not order_id:
result['message'] = '您尚未购买此产品'
return json.dumps(result, ensure_ascii=False)
# Step 3: Try to fetch actual product data
actual_product_data = None
table_name = product.get('product_table_name', '')
table_id = product.get('product_table_id', '')
if table_name and table_id:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except:
pass
# Step 4: Create use record
use_record_id = getID()
result['success'] = True
result['use_record_id'] = use_record_id
result['data'] = {
'product_info': {
'id': product['id'],
'name': product['product_name'],
'code': product['product_code']
},
'actual_data': actual_product_data
}
result['message'] = '产品使用成功'
except Exception as e:
result['message'] = '使用失败: ' + str(e)
return json.dumps(result, ensure_ascii=False, default=str)

View File

@ -0,0 +1,16 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "产品类别管理", "fontSize": "20px", "fontWeight": "bold", "marginBottom": "16px"}
},
{
"widgettype": "Html",
"options": {
"html": "<iframe src=\"{{entire_url('/product_management/product_category_tree')}}\" style=\"width:100%;height:calc(100vh - 200px);border:none;\"></iframe>"
}
}
]
}

65
wwwroot/index.ui Normal file
View File

@ -0,0 +1,65 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "产品管理", "fontSize": "24px", "fontWeight": "bold", "marginBottom": "20px"}
},
{
"widgettype": "ResponsableBox",
"options": {"gap": "16px", "minWidth": "280px"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "cursor": "pointer", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.product_content",
"options": {"url": "{{entire_url('category_manage.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "\uD83D\uDCC1", "fontSize": "32px", "marginBottom": "8px"}},
{"widgettype": "Text", "options": {"text": "产品类别管理", "fontSize": "16px", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "管理产品类别树结构,配置类别属性", "fontSize": "12px", "color": "#666666", "marginTop": "4px"}}
]
},
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "cursor": "pointer", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.product_content",
"options": {"url": "{{entire_url('product_manage.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "\uD83D\uDCE6", "fontSize": "32px", "marginBottom": "8px"}},
{"widgettype": "Text", "options": {"text": "产品管理", "fontSize": "16px", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "添加和管理产品,配置产品信息", "fontSize": "12px", "color": "#666666", "marginTop": "4px"}}
]
},
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "24px", "borderRadius": "8px", "cursor": "pointer", "boxShadow": "0 2px 8px rgba(0,0,0,0.1)"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.product_content",
"options": {"url": "{{entire_url('product_type_config_manage.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "\u2699\uFE0F", "fontSize": "32px", "marginBottom": "8px"}},
{"widgettype": "Text", "options": {"text": "运营商配置", "fontSize": "16px", "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "配置运营商产品类型参数", "fontSize": "12px", "color": "#666666", "marginTop": "4px"}}
]
}
]
},
{
"widgettype": "VBox",
"id": "product_content",
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
}

15
wwwroot/menu.ui Normal file
View File

@ -0,0 +1,15 @@
{
"widgettype": "Menu",
"options": {
"target": "PopupWindow",
"popup_options": {"archor": "cc", "width": "70%", "height": "70%"},
"cwidth": 10,
"items": [
{% if get_user() %}
{"name": "category", "label": "产品类别管理", "url": "{{entire_url('/product_management/category_manage.ui')}}"},
{"name": "product", "label": "产品管理", "url": "{{entire_url('/product_management/product_manage.ui')}}"},
{"name": "config", "label": "运营商配置", "url": "{{entire_url('/product_management/product_type_config_manage.ui')}}"}
{% endif %}
]
}
}

16
wwwroot/product_manage.ui Normal file
View File

@ -0,0 +1,16 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "产品管理", "fontSize": "20px", "fontWeight": "bold", "marginBottom": "16px"}
},
{
"widgettype": "Html",
"options": {
"html": "<iframe src=\"{{entire_url('/product_management/product_list')}}\" style=\"width:100%;height:calc(100vh - 200px);border:none;\"></iframe>"
}
}
]
}

View File

@ -0,0 +1,16 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "16px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "运营商产品类型配置", "fontSize": "20px", "fontWeight": "bold", "marginBottom": "16px"}
},
{
"widgettype": "Html",
"options": {
"html": "<iframe src=\"{{entire_url('/product_management/product_type_config_list')}}\" style=\"width:100%;height:calc(100vh - 200px);border:none;\"></iframe>"
}
}
]
}