feat: initial product_management module - category tree, product registry, operator config, standardized API
This commit is contained in:
parent
e90653bc29
commit
d1ceed9cb9
118
build.sh
Normal file
118
build.sh
Normal 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
38
init/appcodes.json
Normal 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
68
init/data.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
json/product_category_tree.json
Normal file
45
json/product_category_tree.json
Normal 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
52
json/product_list.json
Normal 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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
41
json/product_type_config_list.json
Normal file
41
json/product_type_config_list.json
Normal 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
184
models/product.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
141
models/product_category.json
Normal file
141
models/product_category.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
110
models/product_type_config.json
Normal file
110
models/product_type_config.json
Normal 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'"
|
||||
}
|
||||
]
|
||||
}
|
||||
81
product_management/__init__.py
Normal file
81
product_management/__init__.py
Normal 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
456
product_management/core.py
Normal 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'}
|
||||
4
product_management/init.py
Normal file
4
product_management/init.py
Normal 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
17
pyproject.toml
Normal 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*"]
|
||||
21
wwwroot/api/category_options.dspy
Normal file
21
wwwroot/api/category_options.dspy
Normal 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)
|
||||
72
wwwroot/api/product_brief.dspy
Normal file
72
wwwroot/api/product_brief.dspy
Normal 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)
|
||||
41
wwwroot/api/product_category_create.dspy
Normal file
41
wwwroot/api/product_category_create.dspy
Normal 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)
|
||||
33
wwwroot/api/product_category_delete.dspy
Normal file
33
wwwroot/api/product_category_delete.dspy
Normal 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)
|
||||
32
wwwroot/api/product_category_update.dspy
Normal file
32
wwwroot/api/product_category_update.dspy
Normal 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)
|
||||
45
wwwroot/api/product_create.dspy
Normal file
45
wwwroot/api/product_create.dspy
Normal 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)
|
||||
21
wwwroot/api/product_delete.dspy
Normal file
21
wwwroot/api/product_delete.dspy
Normal 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)
|
||||
109
wwwroot/api/product_detail.dspy
Normal file
109
wwwroot/api/product_detail.dspy
Normal 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)
|
||||
87
wwwroot/api/product_purchase.dspy
Normal file
87
wwwroot/api/product_purchase.dspy
Normal 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)
|
||||
36
wwwroot/api/product_type_config_create.dspy
Normal file
36
wwwroot/api/product_type_config_create.dspy
Normal 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)
|
||||
21
wwwroot/api/product_type_config_delete.dspy
Normal file
21
wwwroot/api/product_type_config_delete.dspy
Normal 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)
|
||||
25
wwwroot/api/product_type_config_update.dspy
Normal file
25
wwwroot/api/product_type_config_update.dspy
Normal 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)
|
||||
34
wwwroot/api/product_update.dspy
Normal file
34
wwwroot/api/product_update.dspy
Normal 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)
|
||||
86
wwwroot/api/product_use.dspy
Normal file
86
wwwroot/api/product_use.dspy
Normal 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)
|
||||
16
wwwroot/category_manage.ui
Normal file
16
wwwroot/category_manage.ui
Normal 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
65
wwwroot/index.ui
Normal 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
15
wwwroot/menu.ui
Normal 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
16
wwwroot/product_manage.ui
Normal 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
16
wwwroot/product_type_config_manage.ui
Normal file
16
wwwroot/product_type_config_manage.ui
Normal 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user