Compare commits
No commits in common. "43787a63a4c3df913d9ed011a05f5122fbba0fdb" and "e90653bc2929d7c64dfe5bd0b13cc81e5d560fa4" have entirely different histories.
43787a63a4
...
e90653bc29
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
product_management.egg-info/
|
|
||||||
__pycache__/
|
|
||||||
132
build.sh
132
build.sh
@ -1,132 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
|
||||||
# Link generated CRUD directories
|
|
||||||
for d in "$MODULE_DIR/wwwroot"/*/; do
|
|
||||||
[ -d "$d" ] || continue
|
|
||||||
dname=$(basename "$d")
|
|
||||||
# Skip api/, styles/, scripts/ - handled separately
|
|
||||||
case "$dname" in
|
|
||||||
api|styles|scripts) continue ;;
|
|
||||||
esac
|
|
||||||
if [ ! -e "$MODULE_WWWROOT/$dname" ]; then
|
|
||||||
ln -sf "$d" "$MODULE_WWWROOT/$dname"
|
|
||||||
echo " Linked CRUD dir: $dname/"
|
|
||||||
fi
|
|
||||||
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"
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"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": "否"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "product_type",
|
|
||||||
"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": "否"},
|
|
||||||
|
|
||||||
{"id": "product_type", "parentid": "", "k": "telecom", "v": "通信服务"},
|
|
||||||
{"id": "product_type", "parentid": "", "k": "cloud", "v": "云服务"},
|
|
||||||
{"id": "product_type", "parentid": "", "k": "data", "v": "数据服务"},
|
|
||||||
{"id": "product_type", "parentid": "", "k": "api", "v": "API服务"},
|
|
||||||
{"id": "product_type", "parentid": "", "k": "custom", "v": "自定义"}
|
|
||||||
],
|
|
||||||
"_note_product_category": "产品类别树由每个 reseller (org_id) 自行管理,不在 init/data.json 中预设全局数据。新机构注册时自动创建根类别。"
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"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')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"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')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
{
|
|
||||||
"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_type",
|
|
||||||
"title": "产品类型标识",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"nullable": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "brief_intro",
|
|
||||||
"title": "产品简介",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "detail_intro",
|
|
||||||
"title": "产品详情",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "extra_json",
|
|
||||||
"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_org_category",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"category_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_product_org_code",
|
|
||||||
"idxtype": "unique",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"product_code"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_product_org_type",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"product_type"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_product_status",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"field": "product_type",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"valuefield": "k",
|
|
||||||
"textfield": "v",
|
|
||||||
"cond": "id='product_type'"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
{
|
|
||||||
"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_type",
|
|
||||||
"title": "产品类型标识",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_type_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_pc_org_parent",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"parent_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_pc_org_status",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"status"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_pc_org_type",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"product_type"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"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'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"field": "product_type",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"valuefield": "k",
|
|
||||||
"textfield": "v",
|
|
||||||
"cond": "id='product_type'"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
{
|
|
||||||
"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_org_opr",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"operator_id"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_ptc_org_cat",
|
|
||||||
"idxtype": "unique",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id",
|
|
||||||
"operator_id",
|
|
||||||
"category_id",
|
|
||||||
"config_name"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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'"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
130
mysql.ddl.sql
130
mysql.ddl.sql
@ -1,130 +0,0 @@
|
|||||||
|
|
||||||
-- ./product_category.json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- 建库时请用以下语句,支持emoji字符
|
|
||||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
drop table if exists product_category;
|
|
||||||
CREATE TABLE product_category
|
|
||||||
(
|
|
||||||
|
|
||||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
|
||||||
`parent_id` VARCHAR(32) DEFAULT '0' comment '父类别ID',
|
|
||||||
`name` VARCHAR(255) NOT NULL comment '类别名称',
|
|
||||||
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '类别描述',
|
|
||||||
`has_product` CHAR(1) DEFAULT '0' comment '是否可挂产品',
|
|
||||||
`product_type` VARCHAR(64) comment '产品类型标识',
|
|
||||||
`product_type_title` VARCHAR(255) comment '产品类型显示名',
|
|
||||||
`sort_order` int DEFAULT '0' comment '排序序号',
|
|
||||||
`icon` VARCHAR(255) comment '图标',
|
|
||||||
`status` CHAR(1) DEFAULT '1' comment '状态',
|
|
||||||
`org_id` VARCHAR(32) DEFAULT '0' comment '所属机构ID',
|
|
||||||
`created_by` VARCHAR(32) comment '创建人',
|
|
||||||
`created_at` datetime NOT NULL comment '创建时间',
|
|
||||||
`updated_at` datetime NOT NULL comment '更新时间'
|
|
||||||
|
|
||||||
|
|
||||||
,primary key(id)
|
|
||||||
|
|
||||||
|
|
||||||
)
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci
|
|
||||||
engine=innodb
|
|
||||||
comment '产品类别树'
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE INDEX product_category_idx_pc_org_parent ON product_category(org_id,parent_id);
|
|
||||||
CREATE INDEX product_category_idx_pc_org_status ON product_category(org_id,status);
|
|
||||||
CREATE INDEX product_category_idx_pc_org_type ON product_category(org_id,product_type);
|
|
||||||
|
|
||||||
-- ./product.json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- 建库时请用以下语句,支持emoji字符
|
|
||||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
drop table if exists product;
|
|
||||||
CREATE TABLE product
|
|
||||||
(
|
|
||||||
|
|
||||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
|
||||||
`category_id` VARCHAR(32) NOT NULL comment '类别ID',
|
|
||||||
`product_code` VARCHAR(64) NOT NULL comment '产品编码',
|
|
||||||
`product_name` VARCHAR(255) NOT NULL comment '产品名称',
|
|
||||||
`product_type` VARCHAR(64) NOT NULL comment '产品类型标识',
|
|
||||||
`brief_intro` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '产品简介',
|
|
||||||
`detail_intro` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '产品详情',
|
|
||||||
`extra_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '扩展属性',
|
|
||||||
`enabled_date` date comment '启用日期',
|
|
||||||
`expired_date` date comment '失效日期',
|
|
||||||
`status` CHAR(1) DEFAULT '1' comment '状态',
|
|
||||||
`price_type` CHAR(1) DEFAULT '1' comment '价格类型',
|
|
||||||
`price` double(15,2) DEFAULT '0.00' comment '价格',
|
|
||||||
`currency` CHAR(8) DEFAULT 'CNY' comment '货币',
|
|
||||||
`sort_order` int DEFAULT '0' comment '排序序号',
|
|
||||||
`org_id` VARCHAR(32) DEFAULT '0' comment '所属机构ID',
|
|
||||||
`created_by` VARCHAR(32) comment '创建人',
|
|
||||||
`created_at` datetime NOT NULL comment '创建时间',
|
|
||||||
`updated_at` datetime NOT NULL comment '更新时间'
|
|
||||||
|
|
||||||
|
|
||||||
,primary key(id)
|
|
||||||
|
|
||||||
|
|
||||||
)
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci
|
|
||||||
engine=innodb
|
|
||||||
comment '产品注册表'
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE INDEX product_idx_product_org_category ON product(org_id,category_id);
|
|
||||||
CREATE UNIQUE INDEX product_idx_product_org_code ON product(org_id,product_code);
|
|
||||||
CREATE INDEX product_idx_product_org_type ON product(org_id,product_type);
|
|
||||||
CREATE INDEX product_idx_product_status ON product(status);
|
|
||||||
CREATE INDEX product_idx_product_enabled_expired ON product(enabled_date,expired_date);
|
|
||||||
|
|
||||||
-- ./product_type_config.json
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- 建库时请用以下语句,支持emoji字符
|
|
||||||
-- CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
drop table if exists product_type_config;
|
|
||||||
CREATE TABLE product_type_config
|
|
||||||
(
|
|
||||||
|
|
||||||
`id` VARCHAR(32) NOT NULL comment '主键ID',
|
|
||||||
`operator_id` VARCHAR(32) NOT NULL comment '运营商用户ID',
|
|
||||||
`org_id` VARCHAR(32) NOT NULL comment '所属机构ID',
|
|
||||||
`category_id` VARCHAR(32) NOT NULL comment '产品类别ID',
|
|
||||||
`config_name` VARCHAR(255) NOT NULL comment '配置名称',
|
|
||||||
`config_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci comment '配置内容',
|
|
||||||
`enabled_flg` CHAR(1) DEFAULT '1' comment '是否启用',
|
|
||||||
`created_by` VARCHAR(32) comment '创建人',
|
|
||||||
`created_at` datetime NOT NULL comment '创建时间',
|
|
||||||
`updated_at` datetime NOT NULL comment '更新时间'
|
|
||||||
|
|
||||||
|
|
||||||
,primary key(id)
|
|
||||||
|
|
||||||
|
|
||||||
)
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci
|
|
||||||
engine=innodb
|
|
||||||
comment '运营商产品类型配置'
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE INDEX product_type_config_idx_ptc_org_opr ON product_type_config(org_id,operator_id);
|
|
||||||
CREATE UNIQUE INDEX product_type_config_idx_ptc_org_cat ON product_type_config(org_id,operator_id,category_id,config_name);
|
|
||||||
CREATE INDEX product_type_config_idx_ptc_enabled ON product_type_config(enabled_flg);
|
|
||||||
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
Metadata-Version: 2.4
|
|
||||||
Name: product_management
|
|
||||||
Version: 1.0.0
|
|
||||||
Summary: Sage product management module - dynamic category tree, product registry, operator config, standardized API
|
|
||||||
Requires-Python: >=3.8
|
|
||||||
Requires-Dist: sqlor
|
|
||||||
Requires-Dist: bricks_for_python
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
README.md
|
|
||||||
pyproject.toml
|
|
||||||
product_management/__init__.py
|
|
||||||
product_management/core.py
|
|
||||||
product_management/init.py
|
|
||||||
product_management.egg-info/PKG-INFO
|
|
||||||
product_management.egg-info/SOURCES.txt
|
|
||||||
product_management.egg-info/dependency_links.txt
|
|
||||||
product_management.egg-info/requires.txt
|
|
||||||
product_management.egg-info/top_level.txt
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
sqlor
|
|
||||||
bricks_for_python
|
|
||||||
@ -1 +0,0 @@
|
|||||||
product_management
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
"""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
|
|
||||||
@ -1,492 +0,0 @@
|
|||||||
"""Product Management Core Business Logic - org_id isolated per reseller"""
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
MODULE_NAME = "product_management"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductManager:
|
|
||||||
"""Core manager for product catalog, category tree, and operator configs.
|
|
||||||
|
|
||||||
All operations are scoped to org_id (reseller institution).
|
|
||||||
Different resellers have completely independent category trees and products.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _get_dbname(self):
|
|
||||||
"""Get module database name from ServerEnv."""
|
|
||||||
env = ServerEnv()
|
|
||||||
return env.get_module_dbname(MODULE_NAME)
|
|
||||||
|
|
||||||
def _get_current_org_id(self):
|
|
||||||
"""Get current user's organization ID from ServerEnv."""
|
|
||||||
env = ServerEnv()
|
|
||||||
return getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
|
|
||||||
|
|
||||||
async def get_category_tree(self, org_id=None):
|
|
||||||
"""Get full category tree for a specific org (reseller)."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
sql = """SELECT * FROM product_category
|
|
||||||
WHERE org_id = ${org_id}$ AND status = '1'
|
|
||||||
ORDER BY sort_order ASC, name ASC"""
|
|
||||||
rows = await sor.sqlExe(sql, {'org_id': org_id})
|
|
||||||
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, org_id=None, status='1'):
|
|
||||||
"""Get all products under a category for a specific org (reseller)."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
|
|
||||||
# Get all sub-category IDs recursively within same org
|
|
||||||
async with DBPools().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}$ AND org_id = ${org_id}$",
|
|
||||||
{'pid': parent, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
for c in (children or []):
|
|
||||||
cid = c['id']
|
|
||||||
all_ids.append(cid)
|
|
||||||
queue.append(cid)
|
|
||||||
|
|
||||||
if not all_ids:
|
|
||||||
return {'success': True, 'products': [], 'total': 0}
|
|
||||||
|
|
||||||
# Use IN clause with proper parameterization
|
|
||||||
param_keys = []
|
|
||||||
params = {'org_id': org_id}
|
|
||||||
for i, cid in enumerate(all_ids):
|
|
||||||
key = f'cid_{i}'
|
|
||||||
param_keys.append(f'${key}$')
|
|
||||||
params[key] = cid
|
|
||||||
|
|
||||||
placeholders = ','.join(param_keys)
|
|
||||||
sql = f"""SELECT p.*, pc.name as category_name
|
|
||||||
FROM product p
|
|
||||||
LEFT JOIN product_category pc ON p.category_id = pc.id AND p.org_id = pc.org_id
|
|
||||||
WHERE p.category_id IN ({placeholders})
|
|
||||||
AND p.org_id = ${org_id}$
|
|
||||||
AND p.status = ${status}$
|
|
||||||
ORDER BY p.sort_order ASC, p.created_at DESC"""
|
|
||||||
params['status'] = 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
|
|
||||||
# Parse extra_json
|
|
||||||
extra_str = r.get('extra_json', '')
|
|
||||||
if extra_str:
|
|
||||||
try:
|
|
||||||
r['extra_parsed'] = json.loads(extra_str)
|
|
||||||
except:
|
|
||||||
r['extra_parsed'] = {}
|
|
||||||
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, org_id=None):
|
|
||||||
"""Get product brief for current org (reseller)."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
|
|
||||||
conditions = ["p.status = '1'", "p.org_id = ${org_id}$"]
|
|
||||||
params = {'org_id': org_id}
|
|
||||||
|
|
||||||
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 DBPools().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_type, p.extra_json
|
|
||||||
FROM product p
|
|
||||||
LEFT JOIN product_category pc ON p.category_id = pc.id AND p.org_id = pc.org_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, org_id=None, user_id=None):
|
|
||||||
"""Get product detail for current org (reseller).
|
|
||||||
|
|
||||||
Returns product_info + category_info + operator_config + extra_parsed.
|
|
||||||
No physical table routing - all data comes from product table + extra_json.
|
|
||||||
"""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
if not user_id:
|
|
||||||
env = ServerEnv()
|
|
||||||
try:
|
|
||||||
user_id = await env.get_user()
|
|
||||||
except:
|
|
||||||
user_id = 'anonymous'
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
|
|
||||||
conditions = ["p.org_id = ${org_id}$"]
|
|
||||||
params = {'org_id': org_id}
|
|
||||||
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 DBPools().sqlorContext(dbname) as sor:
|
|
||||||
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 AND p.org_id = pc.org_id
|
|
||||||
WHERE {where_clause}"""
|
|
||||||
rows = await sor.sqlExe(sql, params)
|
|
||||||
if not rows:
|
|
||||||
return {'success': False, 'error': 'Product not found or no access'}
|
|
||||||
|
|
||||||
product_info = dict(rows[0])
|
|
||||||
|
|
||||||
# Parse extra_json
|
|
||||||
extra_parsed = {}
|
|
||||||
extra_str = product_info.get('extra_json', '')
|
|
||||||
if extra_str:
|
|
||||||
try:
|
|
||||||
extra_parsed = json.loads(extra_str)
|
|
||||||
except:
|
|
||||||
extra_parsed = {'_raw': extra_str}
|
|
||||||
product_info['extra_parsed'] = extra_parsed
|
|
||||||
|
|
||||||
# Get operator config for this category
|
|
||||||
config_sql = """SELECT * FROM product_type_config
|
|
||||||
WHERE category_id = ${category_id}$
|
|
||||||
AND org_id = ${org_id}$
|
|
||||||
AND enabled_flg = '1'
|
|
||||||
AND (operator_id = ${user_id}$ OR operator_id = '0')
|
|
||||||
ORDER BY created_at DESC LIMIT 1"""
|
|
||||||
config_rows = await sor.sqlExe(config_sql, {
|
|
||||||
'category_id': product_info['category_id'],
|
|
||||||
'org_id': org_id,
|
|
||||||
'user_id': user_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'] = {}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async def purchase_product(self, product_id, quantity=1, purchase_data=None, org_id=None, user_id=None):
|
|
||||||
"""Purchase a product within current org (reseller)."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
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'}
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
quantity = int(quantity) if quantity else 1
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
|
|
||||||
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
|
|
||||||
if not rows:
|
|
||||||
return {'success': False, 'message': 'Product not found or no access'}
|
|
||||||
|
|
||||||
product = dict(rows[0])
|
|
||||||
|
|
||||||
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'}
|
|
||||||
|
|
||||||
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,
|
|
||||||
'buyer_org_id': org_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:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'order_id': order_id,
|
|
||||||
'message': 'Purchase request submitted'
|
|
||||||
}
|
|
||||||
|
|
||||||
async def use_product(self, product_id, order_id=None, use_data=None, org_id=None, user_id=None):
|
|
||||||
"""Use a product within current org (reseller)."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
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'}
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
|
|
||||||
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
|
|
||||||
if not rows:
|
|
||||||
return {'success': False, 'message': 'Product not found or no access'}
|
|
||||||
|
|
||||||
product = dict(rows[0])
|
|
||||||
|
|
||||||
# Parse extra_json
|
|
||||||
extra_parsed = {}
|
|
||||||
extra_str = product.get('extra_json', '')
|
|
||||||
if extra_str:
|
|
||||||
try:
|
|
||||||
extra_parsed = json.loads(extra_str)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Verify purchase (if table exists)
|
|
||||||
try:
|
|
||||||
purchase_sql = """SELECT * FROM purchase_orders
|
|
||||||
WHERE product_id = ${product_id}$
|
|
||||||
AND buyer_id = ${user_id}$
|
|
||||||
AND buyer_org_id = ${org_id}$
|
|
||||||
AND status IN ('active', 'pending')"""
|
|
||||||
purchases = await sor.sqlExe(purchase_sql, {
|
|
||||||
'product_id': product_id,
|
|
||||||
'user_id': user_id,
|
|
||||||
'org_id': org_id
|
|
||||||
})
|
|
||||||
if not purchases and not order_id:
|
|
||||||
return {'success': False, 'message': 'Product not purchased'}
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
|
||||||
'success': True,
|
|
||||||
'data': {
|
|
||||||
'product_info': {
|
|
||||||
'id': product['id'],
|
|
||||||
'name': product['product_name'],
|
|
||||||
'code': product['product_code'],
|
|
||||||
'product_type': product.get('product_type', '')
|
|
||||||
},
|
|
||||||
'extra_parsed': extra_parsed
|
|
||||||
},
|
|
||||||
'message': 'Product use successful'
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_operator_config(self, category_id, org_id=None, user_id=None):
|
|
||||||
"""Get operator configuration for a category within current org."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
if not user_id:
|
|
||||||
env = ServerEnv()
|
|
||||||
try:
|
|
||||||
user_id = await env.get_user()
|
|
||||||
except:
|
|
||||||
user_id = 'anonymous'
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
sql = """SELECT * FROM product_type_config
|
|
||||||
WHERE category_id = ${category_id}$
|
|
||||||
AND org_id = ${org_id}$
|
|
||||||
AND enabled_flg = '1'
|
|
||||||
AND (operator_id = ${user_id}$ OR operator_id = '0')
|
|
||||||
ORDER BY created_at DESC"""
|
|
||||||
rows = await sor.sqlExe(sql, {
|
|
||||||
'category_id': category_id,
|
|
||||||
'org_id': org_id,
|
|
||||||
'user_id': user_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, org_id=None, user_id=None):
|
|
||||||
"""Create or update operator configuration within current org."""
|
|
||||||
if not org_id:
|
|
||||||
org_id = self._get_current_org_id()
|
|
||||||
if not user_id:
|
|
||||||
env = ServerEnv()
|
|
||||||
try:
|
|
||||||
user_id = await env.get_user()
|
|
||||||
except:
|
|
||||||
return {'success': False, 'message': 'User not authenticated'}
|
|
||||||
|
|
||||||
if not config_name:
|
|
||||||
return {'success': False, 'message': 'Missing config_name'}
|
|
||||||
|
|
||||||
dbname = self._get_dbname()
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
try:
|
|
||||||
json.loads(config_json)
|
|
||||||
except:
|
|
||||||
return {'success': False, 'message': 'Invalid config_json format'}
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
# Verify category belongs to org
|
|
||||||
cat_check = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product_category WHERE id = ${category_id}$ AND org_id = ${org_id}$",
|
|
||||||
{'category_id': category_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not cat_check:
|
|
||||||
return {'success': False, 'message': 'Category not found or no access'}
|
|
||||||
|
|
||||||
existing = await sor.sqlExe(
|
|
||||||
"""SELECT id FROM product_type_config
|
|
||||||
WHERE category_id = ${category_id}$
|
|
||||||
AND org_id = ${org_id}$
|
|
||||||
AND operator_id = ${user_id}$
|
|
||||||
AND config_name = ${config_name}$""",
|
|
||||||
{'category_id': category_id, 'org_id': org_id,
|
|
||||||
'user_id': user_id, 'config_name': config_name}
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing:
|
|
||||||
config_id = existing[0]['id']
|
|
||||||
await sor.U('product_type_config', {
|
|
||||||
'config_json': config_json,
|
|
||||||
'updated_at': now
|
|
||||||
}, {'id': config_id, 'org_id': org_id})
|
|
||||||
return {'success': True, 'id': config_id, 'message': 'Config updated'}
|
|
||||||
else:
|
|
||||||
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'}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
"""Product Management Module - load function for Sage"""
|
|
||||||
from product_management import load_product_management
|
|
||||||
|
|
||||||
__all__ = ['load_product_management']
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
[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*"]
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
product_management 模块 RBAC 权限管理脚本
|
|
||||||
|
|
||||||
使用方法:
|
|
||||||
cd ~/repos/sage
|
|
||||||
./py3/bin/python ~/repos/product_management/scripts/load_path.py
|
|
||||||
|
|
||||||
每次代码变更如有新 path 出现,需同步更新此脚本。
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def find_sage_root():
|
|
||||||
candidates = [
|
|
||||||
os.path.expanduser("~/repos/sage"),
|
|
||||||
os.path.expanduser("~/sage"),
|
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
|
|
||||||
]
|
|
||||||
for c in candidates:
|
|
||||||
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
|
|
||||||
return c
|
|
||||||
return None
|
|
||||||
|
|
||||||
SAGE_ROOT = find_sage_root()
|
|
||||||
if not SAGE_ROOT:
|
|
||||||
print("ERROR: Cannot find Sage root directory")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
|
|
||||||
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
|
|
||||||
|
|
||||||
MOD = "product_management"
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 权限路径定义 — 每次新增页面或API时同步更新
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
# any — 无需登录(仅静态资源和菜单)
|
|
||||||
PATHS_ANY = [
|
|
||||||
f"/{MOD}/menu.ui",
|
|
||||||
]
|
|
||||||
|
|
||||||
# logined — 需要认证的页面和 API
|
|
||||||
PATHS_LOGINED = [
|
|
||||||
# Module entry
|
|
||||||
f"/{MOD}",
|
|
||||||
f"/{MOD}/index.ui",
|
|
||||||
|
|
||||||
# Feature pages
|
|
||||||
f"/{MOD}/category_manage.ui",
|
|
||||||
f"/{MOD}/product_manage.ui",
|
|
||||||
f"/{MOD}/product_type_config_manage.ui",
|
|
||||||
|
|
||||||
# CRUD alias directories (directory path + index.ui path)
|
|
||||||
f"/{MOD}/product_category_tree",
|
|
||||||
f"/{MOD}/product_category_tree/index.ui",
|
|
||||||
f"/{MOD}/product_list",
|
|
||||||
f"/{MOD}/product_list/index.ui",
|
|
||||||
f"/{MOD}/product_type_config_list",
|
|
||||||
f"/{MOD}/product_type_config_list/index.ui",
|
|
||||||
|
|
||||||
# API endpoints (.dspy)
|
|
||||||
f"/{MOD}/api/category_options.dspy",
|
|
||||||
f"/{MOD}/api/product_brief.dspy",
|
|
||||||
f"/{MOD}/api/product_category_create.dspy",
|
|
||||||
f"/{MOD}/api/product_category_update.dspy",
|
|
||||||
f"/{MOD}/api/product_category_delete.dspy",
|
|
||||||
f"/{MOD}/api/product_create.dspy",
|
|
||||||
f"/{MOD}/api/product_update.dspy",
|
|
||||||
f"/{MOD}/api/product_delete.dspy",
|
|
||||||
f"/{MOD}/api/product_detail.dspy",
|
|
||||||
f"/{MOD}/api/product_purchase.dspy",
|
|
||||||
f"/{MOD}/api/product_use.dspy",
|
|
||||||
f"/{MOD}/api/product_type_config_create.dspy",
|
|
||||||
f"/{MOD}/api/product_type_config_update.dspy",
|
|
||||||
f"/{MOD}/api/product_type_config_delete.dspy",
|
|
||||||
|
|
||||||
# CRUD auto-generated .dspy
|
|
||||||
f"/{MOD}/product_category_tree/get_product_category.dspy",
|
|
||||||
f"/{MOD}/product_category_tree/new_product_category.dspy",
|
|
||||||
f"/{MOD}/product_category_tree/update_product_category.dspy",
|
|
||||||
f"/{MOD}/product_category_tree/delete_product_category.dspy",
|
|
||||||
f"/{MOD}/product_list/get_product.dspy",
|
|
||||||
f"/{MOD}/product_list/add_product.dspy",
|
|
||||||
f"/{MOD}/product_list/update_product.dspy",
|
|
||||||
f"/{MOD}/product_list/delete_product.dspy",
|
|
||||||
f"/{MOD}/product_type_config_list/get_product_type_config.dspy",
|
|
||||||
f"/{MOD}/product_type_config_list/add_product_type_config.dspy",
|
|
||||||
f"/{MOD}/product_type_config_list/update_product_type_config.dspy",
|
|
||||||
f"/{MOD}/product_type_config_list/delete_product_type_config.dspy",
|
|
||||||
]
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 执行注册
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def run_set_perm(role, path):
|
|
||||||
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
return result.returncode == 0
|
|
||||||
|
|
||||||
def register_role_paths(role, paths):
|
|
||||||
count = 0
|
|
||||||
for p in paths:
|
|
||||||
if run_set_perm(role, p):
|
|
||||||
count += 1
|
|
||||||
print(f" {role}: {count}/{len(paths)} paths registered")
|
|
||||||
return count
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print(f"Sage root: {SAGE_ROOT}")
|
|
||||||
total = 0
|
|
||||||
total += register_role_paths("any", PATHS_ANY)
|
|
||||||
total += register_role_paths("logined", PATHS_LOGINED)
|
|
||||||
print(f"\nDone. Total {total} permission entries registered.")
|
|
||||||
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'success': False, 'data': []}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
sql = """SELECT id, name FROM product_category
|
|
||||||
WHERE has_product='1' AND status='1' AND org_id = ${org_id}$
|
|
||||||
ORDER BY sort_order ASC, name ASC"""
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.sqlExe(sql, {'org_id': org_id})
|
|
||||||
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)
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
产品简介标准化接口 (按机构隔离)
|
|
||||||
参数:
|
|
||||||
product_id: 产品ID (product表id)
|
|
||||||
product_code: 产品编码 (可选)
|
|
||||||
category_id: 类别ID (可选,返回该类别下所有产品)
|
|
||||||
org_id: 机构ID (可选,不传则用当前用户机构)
|
|
||||||
返回:
|
|
||||||
{success, data: [{id, product_code, product_name, category_name, brief_intro, price, enabled_date, expired_date, status, extra_json}]}
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'success': False, 'data': [], 'total': 0}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
product_id = params_kw.get('product_id', '')
|
|
||||||
product_code = params_kw.get('product_code', '')
|
|
||||||
category_id = params_kw.get('category_id', '')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
conditions = ["p.status = '1'", "p.org_id = ${org_id}$"]
|
|
||||||
params = {'org_id': org_id}
|
|
||||||
|
|
||||||
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_type, p.extra_json
|
|
||||||
FROM product p
|
|
||||||
LEFT JOIN product_category pc ON p.category_id = pc.id AND p.org_id = pc.org_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 []
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
today = datetime.date.today().isoformat()
|
|
||||||
active_products = []
|
|
||||||
for r in rows:
|
|
||||||
r = dict(r)
|
|
||||||
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'] = 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)
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
data['id'] = getID()
|
|
||||||
data['org_id'] = org_id
|
|
||||||
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'
|
|
||||||
|
|
||||||
# Validate parent belongs to same org
|
|
||||||
if data['parent_id'] != '0':
|
|
||||||
check_sql = "SELECT id FROM product_category WHERE id = ${parent_id}$ AND org_id = ${org_id}$"
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.sqlExe(check_sql, {'parent_id': data['parent_id'], 'org_id': org_id})
|
|
||||||
if not rows:
|
|
||||||
raise ValueError('父类别不存在或不属于当前机构')
|
|
||||||
|
|
||||||
if data.get('has_product') == '0':
|
|
||||||
data['product_type'] = ''
|
|
||||||
data['product_type_title'] = ''
|
|
||||||
|
|
||||||
fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_type', 'product_type_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)
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
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:
|
|
||||||
# Verify belongs to org
|
|
||||||
check = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product_category WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权删除该记录')
|
|
||||||
|
|
||||||
# Check children
|
|
||||||
children = await sor.sqlExe(
|
|
||||||
"SELECT COUNT(*) as cnt FROM product_category WHERE parent_id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_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 products
|
|
||||||
products = await sor.sqlExe(
|
|
||||||
"SELECT COUNT(*) as cnt FROM product WHERE category_id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_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, 'org_id': org_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)
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
record_id = data.pop('id', None)
|
|
||||||
if not record_id:
|
|
||||||
raise ValueError('Missing id')
|
|
||||||
|
|
||||||
# Verify record belongs to current org
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
check = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product_category WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权操作该记录')
|
|
||||||
|
|
||||||
data['updated_at'] = now
|
|
||||||
if data.get('has_product') == '0':
|
|
||||||
data['product_type'] = ''
|
|
||||||
data['product_type_title'] = ''
|
|
||||||
|
|
||||||
fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_type', 'product_type_title', 'parent_id')}
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
await sor.U('product_category', fields, {'id': record_id, 'org_id': org_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)
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
data['id'] = getID()
|
|
||||||
data['org_id'] = org_id
|
|
||||||
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'
|
|
||||||
|
|
||||||
# Verify category belongs to current org
|
|
||||||
if data.get('category_id'):
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
cat_check = await sor.sqlExe(
|
|
||||||
"SELECT id, product_type FROM product_category WHERE id = ${category_id}$ AND org_id = ${org_id}$",
|
|
||||||
{'category_id': data['category_id'], 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not cat_check:
|
|
||||||
raise ValueError('类别不存在或不属于当前机构')
|
|
||||||
# Auto-fill product_type from category
|
|
||||||
if not data.get('product_type'):
|
|
||||||
data['product_type'] = cat_check[0].get('product_type', '')
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
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 = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权删除该记录')
|
|
||||||
|
|
||||||
await sor.D('product', {'id': record_id, 'org_id': org_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)
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
产品详情标准化接口 (按机构隔离)
|
|
||||||
参数:
|
|
||||||
product_id: 产品ID
|
|
||||||
product_code: 产品编码 (可选)
|
|
||||||
org_id: 机构ID (可选,不传则用当前用户机构)
|
|
||||||
返回:
|
|
||||||
{success, data: {product_info, category_info, operator_config, extra_parsed}}
|
|
||||||
说明:
|
|
||||||
product_info 包含产品全部信息
|
|
||||||
extra_parsed 是 extra_json 解析后的结构化数据
|
|
||||||
operator_config 是当前运营商对该产品类型的配置
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'success': False, 'data': {}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
conditions = ["p.org_id = ${org_id}$"]
|
|
||||||
params = {'org_id': org_id}
|
|
||||||
|
|
||||||
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
|
|
||||||
FROM product p
|
|
||||||
LEFT JOIN product_category pc ON p.category_id = pc.id AND p.org_id = pc.org_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])
|
|
||||||
|
|
||||||
# Parse extra_json
|
|
||||||
extra_parsed = {}
|
|
||||||
extra_str = product_info.get('extra_json', '')
|
|
||||||
if extra_str:
|
|
||||||
try:
|
|
||||||
extra_parsed = json.loads(extra_str)
|
|
||||||
except:
|
|
||||||
extra_parsed = {'_raw': extra_str}
|
|
||||||
product_info['extra_parsed'] = extra_parsed
|
|
||||||
|
|
||||||
# Get operator config for this product_type
|
|
||||||
config_sql = """SELECT * FROM product_type_config
|
|
||||||
WHERE category_id = ${category_id}$
|
|
||||||
AND enabled_flg = '1'
|
|
||||||
AND org_id = ${org_id}$
|
|
||||||
AND (operator_id = ${user_id}$ OR operator_id = '0')
|
|
||||||
ORDER BY created_at DESC LIMIT 1"""
|
|
||||||
config_rows = await sor.sqlExe(config_sql, {
|
|
||||||
'category_id': product_info['category_id'],
|
|
||||||
'org_id': org_id,
|
|
||||||
'user_id': user_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'] = {}
|
|
||||||
|
|
||||||
result['data'] = {
|
|
||||||
'product_info': product_info,
|
|
||||||
'category_info': {
|
|
||||||
'name': product_info.get('category_name'),
|
|
||||||
'description': product_info.get('category_description')
|
|
||||||
},
|
|
||||||
'operator_config': operator_config
|
|
||||||
}
|
|
||||||
result['success'] = True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
result['error'] = str(e)
|
|
||||||
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
产品购买标准化接口 (按机构隔离)
|
|
||||||
参数:
|
|
||||||
product_id: 产品ID
|
|
||||||
quantity: 购买数量 (默认1)
|
|
||||||
purchase_data: 购买附加数据 (JSON字符串)
|
|
||||||
org_id: 机构ID (可选)
|
|
||||||
返回:
|
|
||||||
{success, order_id, message}
|
|
||||||
"""
|
|
||||||
import json, time
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
|
|
||||||
result = {'success': False, 'order_id': '', 'message': ''}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
|
|
||||||
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)
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
|
|
||||||
if not rows:
|
|
||||||
result['message'] = '产品不存在或已禁用'
|
|
||||||
return json.dumps(result, ensure_ascii=False)
|
|
||||||
|
|
||||||
product = dict(rows[0])
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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,
|
|
||||||
'buyer_org_id': org_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,
|
|
||||||
'status': 'pending',
|
|
||||||
'created_at': now,
|
|
||||||
'updated_at': now
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
await sor.C('purchase_orders', order_data)
|
|
||||||
except Exception:
|
|
||||||
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)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
data['id'] = getID()
|
|
||||||
data['operator_id'] = user_id
|
|
||||||
data['org_id'] = org_id
|
|
||||||
data['created_by'] = user_id
|
|
||||||
data['created_at'] = now
|
|
||||||
data['updated_at'] = now
|
|
||||||
if 'enabled_flg' not in data:
|
|
||||||
data['enabled_flg'] = '1'
|
|
||||||
|
|
||||||
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)
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
|
|
||||||
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 = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product_type_config WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权删除该记录')
|
|
||||||
|
|
||||||
await sor.D('product_type_config', {'id': record_id, 'org_id': org_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)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
record_id = data.pop('id', None)
|
|
||||||
if not record_id:
|
|
||||||
raise ValueError('Missing id')
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
check = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product_type_config WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权操作该记录')
|
|
||||||
|
|
||||||
data['updated_at'] = now
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
await sor.U('product_type_config', data, {'id': record_id, 'org_id': org_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)
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import json, time
|
|
||||||
|
|
||||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = (await get_userorgid()) or '0'
|
|
||||||
now = time.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
data = dict(params_kw)
|
|
||||||
record_id = data.pop('id', None)
|
|
||||||
if not record_id:
|
|
||||||
raise ValueError('Missing id')
|
|
||||||
|
|
||||||
# Verify belongs to org
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
check = await sor.sqlExe(
|
|
||||||
"SELECT id FROM product WHERE id = ${id}$ AND org_id = ${org_id}$",
|
|
||||||
{'id': record_id, 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if not check:
|
|
||||||
raise ValueError('无权操作该记录')
|
|
||||||
|
|
||||||
data['updated_at'] = now
|
|
||||||
|
|
||||||
# If category changed, update product_type
|
|
||||||
if data.get('category_id') and not data.get('product_type'):
|
|
||||||
cat_check = await sor.sqlExe(
|
|
||||||
"SELECT product_type FROM product_category WHERE id = ${category_id}$ AND org_id = ${org_id}$",
|
|
||||||
{'category_id': data['category_id'], 'org_id': org_id}
|
|
||||||
)
|
|
||||||
if cat_check:
|
|
||||||
data['product_type'] = cat_check[0].get('product_type', '')
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
await sor.U('product', data, {'id': record_id, 'org_id': org_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)
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
产品使用标准化接口 (按机构隔离)
|
|
||||||
参数:
|
|
||||||
product_id: 产品ID
|
|
||||||
order_id: 订单ID (可选)
|
|
||||||
use_data: 使用附加数据 (JSON字符串)
|
|
||||||
org_id: 机构ID (可选)
|
|
||||||
返回:
|
|
||||||
{success, use_record_id, data: {product_info, extra_parsed}}
|
|
||||||
"""
|
|
||||||
import json, time
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
|
|
||||||
result = {'success': False, 'use_record_id': '', 'message': '', 'data': {}}
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_id = await get_user()
|
|
||||||
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
|
|
||||||
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)
|
|
||||||
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
|
|
||||||
|
|
||||||
async with DBPools().sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
|
|
||||||
if not rows:
|
|
||||||
result['message'] = '产品不存在或无权访问'
|
|
||||||
return json.dumps(result, ensure_ascii=False)
|
|
||||||
|
|
||||||
product = dict(rows[0])
|
|
||||||
|
|
||||||
# Parse extra_json for the product
|
|
||||||
extra_parsed = {}
|
|
||||||
extra_str = product.get('extra_json', '')
|
|
||||||
if extra_str:
|
|
||||||
try:
|
|
||||||
extra_parsed = json.loads(extra_str)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Verify purchase (if table exists)
|
|
||||||
try:
|
|
||||||
purchase_sql = """SELECT * FROM purchase_orders
|
|
||||||
WHERE product_id = ${product_id}$
|
|
||||||
AND buyer_id = ${user_id}$
|
|
||||||
AND buyer_org_id = ${org_id}$
|
|
||||||
AND status IN ('active', 'pending')"""
|
|
||||||
purchases = await sor.sqlExe(purchase_sql, {
|
|
||||||
'product_id': product_id,
|
|
||||||
'user_id': user_id,
|
|
||||||
'org_id': org_id
|
|
||||||
})
|
|
||||||
if not purchases and not order_id:
|
|
||||||
result['message'] = '您尚未购买此产品'
|
|
||||||
return json.dumps(result, ensure_ascii=False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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'],
|
|
||||||
'product_type': product.get('product_type', '')
|
|
||||||
},
|
|
||||||
'extra_parsed': extra_parsed
|
|
||||||
}
|
|
||||||
result['message'] = '产品使用成功'
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
result['message'] = '使用失败: ' + str(e)
|
|
||||||
|
|
||||||
return json.dumps(result, ensure_ascii=False, default=str)
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"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>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"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"}}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"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 %}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
ns = {
|
|
||||||
'id':params_kw['id'],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.D('product_category', ns)
|
|
||||||
debug('delete success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Success",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Delete failed');
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
sql = '''select * from product_category where 1 = 1'''
|
|
||||||
id = ns.get('id')
|
|
||||||
if id:
|
|
||||||
sql += " and parent_id = ${id}$"
|
|
||||||
else:
|
|
||||||
sql += " and parent_id is null"
|
|
||||||
|
|
||||||
sql += " order by name "
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.sqlExe(sql, ns)
|
|
||||||
return r
|
|
||||||
return []
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"widgettype":"Tree",
|
|
||||||
"options":{
|
|
||||||
"width":"100%",
|
|
||||||
"height":"100%",
|
|
||||||
|
|
||||||
|
|
||||||
"title":"产品类别树",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"toolbar":{"tools":[{"selected_row":true,"name":"product","icon":"{{entire_url('/imgs/product.svg')}}","label":"下属产品"}]},
|
|
||||||
|
|
||||||
|
|
||||||
"editable":{
|
|
||||||
"fields":[
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"title": "类别名称",
|
|
||||||
"type": "str",
|
|
||||||
"length": 255,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "类别名称"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "description",
|
|
||||||
"title": "类别描述",
|
|
||||||
"type": "text",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "text",
|
|
||||||
"datatype": "text",
|
|
||||||
"label": "类别描述"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "has_product",
|
|
||||||
"title": "是否可挂产品",
|
|
||||||
"type": "char",
|
|
||||||
"length": 1,
|
|
||||||
"default": "0",
|
|
||||||
"label": "是否可挂产品",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "has_product",
|
|
||||||
"textField": "has_product_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "has_product",
|
|
||||||
"textField": "has_product_text",
|
|
||||||
"cond": "id='has_product_flg'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "是"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "0",
|
|
||||||
"text": "否"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_type",
|
|
||||||
"title": "产品类型标识",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"label": "产品类型标识",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "product_type",
|
|
||||||
"textField": "product_type_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "product_type",
|
|
||||||
"textField": "product_type_text",
|
|
||||||
"cond": "id='product_type'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_type_title",
|
|
||||||
"title": "产品类型显示名",
|
|
||||||
"type": "str",
|
|
||||||
"length": 255,
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "产品类型显示名"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sort_order",
|
|
||||||
"title": "排序序号",
|
|
||||||
"type": "int",
|
|
||||||
"default": "0",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "int",
|
|
||||||
"datatype": "int",
|
|
||||||
"label": "排序序号"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "icon",
|
|
||||||
"title": "图标",
|
|
||||||
"type": "str",
|
|
||||||
"length": 255,
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "图标"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "status",
|
|
||||||
"title": "状态",
|
|
||||||
"type": "char",
|
|
||||||
"length": 1,
|
|
||||||
"default": "1",
|
|
||||||
"label": "状态",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "status",
|
|
||||||
"textField": "status_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "status",
|
|
||||||
"textField": "status_text",
|
|
||||||
"cond": "id='product_category_status'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "启用"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "0",
|
|
||||||
"text": "禁用"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"add_url":"{{entire_url('./new_product_category.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"update_url":"{{entire_url('./update_product_category.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"delete_url":"{{entire_url('./delete_product_category.dspy')}}"
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
"parentField":"parent_id",
|
|
||||||
"idField":"id",
|
|
||||||
"textField":"name",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"dataurl":"{{entire_url('./get_product_category.dspy')}}"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
,"binds":[
|
|
||||||
{
|
|
||||||
"wid": "self",
|
|
||||||
"event": "product",
|
|
||||||
"actiontype": "urlwidget",
|
|
||||||
"target": "PopupWindow",
|
|
||||||
"popup_options": {
|
|
||||||
"title": "下属产品",
|
|
||||||
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=product",
|
|
||||||
"resizable": true,
|
|
||||||
"height": "70%",
|
|
||||||
"width": "70%"
|
|
||||||
},
|
|
||||||
"params_mapping": {
|
|
||||||
"mapping": {
|
|
||||||
"id": "category_id",
|
|
||||||
"referer_widget": "referer_widget"
|
|
||||||
},
|
|
||||||
"need_other": false
|
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"method": "POST",
|
|
||||||
"params": {},
|
|
||||||
"url": "{{entire_url('../product_list')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
id = params_kw.id
|
|
||||||
if not id or len(id) > 32:
|
|
||||||
id = uuid()
|
|
||||||
ns['id'] = id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.C('product_category', ns.copy())
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"title":"Add Success",
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Add Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
|
|
||||||
ns1 = {
|
|
||||||
|
|
||||||
"org_id": userorgid,
|
|
||||||
|
|
||||||
|
|
||||||
"id": params_kw.id
|
|
||||||
}
|
|
||||||
recs = await sor.R('product_category', ns1)
|
|
||||||
if len(recs) < 1:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"Record no exist or with wrong ownership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r = await sor.U('product_category', ns)
|
|
||||||
debug('update success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Success",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
id = params_kw.id
|
|
||||||
if not id or len(id) > 32:
|
|
||||||
id = uuid()
|
|
||||||
ns['id'] = id
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.C('product', ns.copy())
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"title":"Add Success",
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Add Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
ns = {
|
|
||||||
'id':params_kw['id'],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.D('product', ns)
|
|
||||||
debug('delete success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Success",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Delete failed');
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,190 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
ns['userorgid'] = userorgid
|
|
||||||
|
|
||||||
debug(f'get_product.dspy:{ns=}')
|
|
||||||
if not ns.get('page'):
|
|
||||||
ns['page'] = 1
|
|
||||||
if not ns.get('sort'):
|
|
||||||
|
|
||||||
|
|
||||||
ns['sort'] = ["sort_order asc","created_at desc"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
sql = '''select a.*, b.category_id_text, c.status_text, d.price_type_text, e.product_type_text
|
|
||||||
from (select * from product where 1=1 [[filterstr]]) a left join (select id as category_id,
|
|
||||||
name as category_id_text from product_category where has_product='1' AND status='1') b on a.category_id = b.category_id left join (select k as status,
|
|
||||||
v as status_text from appcodes_kv where id='product_status') c on a.status = c.status left join (select k as price_type,
|
|
||||||
v as price_type_text from appcodes_kv where id='product_price_type') d on a.price_type = d.price_type left join (select k as product_type,
|
|
||||||
v as product_type_text from appcodes_kv where id='product_type') e on a.product_type = e.product_type'''
|
|
||||||
|
|
||||||
filterjson = params_kw.get('data_filter')
|
|
||||||
fields_str=r'''[
|
|
||||||
{
|
|
||||||
"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_type",
|
|
||||||
"title": "产品类型标识",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"nullable": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "brief_intro",
|
|
||||||
"title": "产品简介",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "detail_intro",
|
|
||||||
"title": "产品详情",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "extra_json",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]'''
|
|
||||||
ori_fields = json.loads(fields_str)
|
|
||||||
if not filterjson:
|
|
||||||
fields = [ f['name'] for f in ori_fields ]
|
|
||||||
filterjson = default_filterjson(fields, ns)
|
|
||||||
filterdic = ns.copy()
|
|
||||||
filterdic['filterstr'] = ''
|
|
||||||
filterdic['userorgid'] = '${userorgid}$'
|
|
||||||
filterdic['userid'] = '${userid}$'
|
|
||||||
if filterjson:
|
|
||||||
dbf = DBFilter(filterjson)
|
|
||||||
conds = dbf.gen(ns)
|
|
||||||
if conds:
|
|
||||||
ns.update(dbf.consts)
|
|
||||||
conds = f' and {conds}'
|
|
||||||
filterdic['filterstr'] = conds
|
|
||||||
ac = ArgsConvert('[[', ']]')
|
|
||||||
vars = ac.findAllVariables(sql)
|
|
||||||
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
|
||||||
filterdic.update(NameSpace)
|
|
||||||
sql = ac.convert(sql, filterdic)
|
|
||||||
|
|
||||||
debug(f'{sql=}')
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.sqlPaging(sql, ns)
|
|
||||||
return r
|
|
||||||
return {
|
|
||||||
"total":0,
|
|
||||||
"rows":[]
|
|
||||||
}
|
|
||||||
@ -1,361 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"id":"product_tbl",
|
|
||||||
"widgettype":"Tabular",
|
|
||||||
"options":{
|
|
||||||
"width":"100%",
|
|
||||||
"height":"100%",
|
|
||||||
|
|
||||||
|
|
||||||
"title":"产品注册表",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"css":"card",
|
|
||||||
|
|
||||||
|
|
||||||
"editable":{
|
|
||||||
|
|
||||||
"new_data_url":"{{entire_url('add_product.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"delete_data_url":"{{entire_url('delete_product.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"update_data_url":"{{entire_url('update_product.dspy')}}"
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
"data_url":"{{entire_url('./get_product.dspy')}}",
|
|
||||||
|
|
||||||
"data_method":"GET",
|
|
||||||
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
|
||||||
"row_options":{
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"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"
|
|
||||||
],
|
|
||||||
|
|
||||||
"fields":[
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"title": "主键ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "主键ID"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "category_id",
|
|
||||||
"title": "类别ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"label": "类别ID",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "category_id",
|
|
||||||
"textField": "category_id_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "product_category",
|
|
||||||
"tblvalue": "id",
|
|
||||||
"tbltext": "name",
|
|
||||||
"valueField": "category_id",
|
|
||||||
"textField": "category_id_text",
|
|
||||||
"cond": "has_product='1' AND status='1'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('../api/category_options.dspy')}}",
|
|
||||||
"datamethod": "GET"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_code",
|
|
||||||
"title": "产品编码",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "产品编码"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_name",
|
|
||||||
"title": "产品名称",
|
|
||||||
"type": "str",
|
|
||||||
"length": 255,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "产品名称"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "product_type",
|
|
||||||
"title": "产品类型标识",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"nullable": "no",
|
|
||||||
"label": "产品类型标识",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "product_type",
|
|
||||||
"textField": "product_type_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "product_type",
|
|
||||||
"textField": "product_type_text",
|
|
||||||
"cond": "id='product_type'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "brief_intro",
|
|
||||||
"title": "产品简介",
|
|
||||||
"type": "text",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "text",
|
|
||||||
"datatype": "text",
|
|
||||||
"label": "产品简介"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "detail_intro",
|
|
||||||
"title": "产品详情",
|
|
||||||
"type": "text",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "text",
|
|
||||||
"datatype": "text",
|
|
||||||
"label": "产品详情"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "extra_json",
|
|
||||||
"title": "扩展属性",
|
|
||||||
"type": "text",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "text",
|
|
||||||
"datatype": "text",
|
|
||||||
"label": "扩展属性"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enabled_date",
|
|
||||||
"title": "启用日期",
|
|
||||||
"type": "date",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "date",
|
|
||||||
"datatype": "date",
|
|
||||||
"label": "启用日期"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "expired_date",
|
|
||||||
"title": "失效日期",
|
|
||||||
"type": "date",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "date",
|
|
||||||
"datatype": "date",
|
|
||||||
"label": "失效日期"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "status",
|
|
||||||
"title": "状态",
|
|
||||||
"type": "char",
|
|
||||||
"length": 1,
|
|
||||||
"default": "1",
|
|
||||||
"label": "状态",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "status",
|
|
||||||
"textField": "status_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "status",
|
|
||||||
"textField": "status_text",
|
|
||||||
"cond": "id='product_status'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "启用"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "0",
|
|
||||||
"text": "禁用"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "price_type",
|
|
||||||
"title": "价格类型",
|
|
||||||
"type": "char",
|
|
||||||
"length": 1,
|
|
||||||
"default": "1",
|
|
||||||
"label": "价格类型",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "price_type",
|
|
||||||
"textField": "price_type_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "price_type",
|
|
||||||
"textField": "price_type_text",
|
|
||||||
"cond": "id='product_price_type'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "固定价格"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "2",
|
|
||||||
"text": "阶梯价格"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "3",
|
|
||||||
"text": "议价"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "price",
|
|
||||||
"title": "价格",
|
|
||||||
"type": "double",
|
|
||||||
"length": 15,
|
|
||||||
"dec": 2,
|
|
||||||
"default": "0.00",
|
|
||||||
"cwidth": 15,
|
|
||||||
"uitype": "float",
|
|
||||||
"datatype": "double",
|
|
||||||
"label": "价格"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "currency",
|
|
||||||
"title": "货币",
|
|
||||||
"type": "char",
|
|
||||||
"length": 8,
|
|
||||||
"default": "CNY",
|
|
||||||
"cwidth": 8,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "char",
|
|
||||||
"label": "货币"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "sort_order",
|
|
||||||
"title": "排序序号",
|
|
||||||
"type": "int",
|
|
||||||
"default": "0",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "int",
|
|
||||||
"datatype": "int",
|
|
||||||
"label": "排序序号"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "org_id",
|
|
||||||
"title": "所属机构ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"default": "0",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "所属机构ID"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_by",
|
|
||||||
"title": "创建人",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "创建人"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"title": "创建时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "no",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "datetime",
|
|
||||||
"label": "创建时间"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updated_at",
|
|
||||||
"title": "更新时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "no",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "datetime",
|
|
||||||
"label": "更新时间"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"page_rows":160,
|
|
||||||
"cache_limit":5
|
|
||||||
}
|
|
||||||
|
|
||||||
,"binds":[]
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
|
|
||||||
ns1 = {
|
|
||||||
|
|
||||||
"org_id": userorgid,
|
|
||||||
|
|
||||||
|
|
||||||
"id": params_kw.id
|
|
||||||
}
|
|
||||||
recs = await sor.R('product', ns1)
|
|
||||||
if len(recs) < 1:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"Record no exist or with wrong ownership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r = await sor.U('product', ns)
|
|
||||||
debug('update success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Success",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"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>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
id = params_kw.id
|
|
||||||
if not id or len(id) > 32:
|
|
||||||
id = uuid()
|
|
||||||
ns['id'] = id
|
|
||||||
|
|
||||||
|
|
||||||
userid = await get_user()
|
|
||||||
if not userid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['operator_id'] = userid
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.C('product_type_config', ns.copy())
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"title":"Add Success",
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Add Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
|
|
||||||
ns = {
|
|
||||||
'id':params_kw['id'],
|
|
||||||
}
|
|
||||||
|
|
||||||
userid = await get_user()
|
|
||||||
if not userid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['operator_id'] = userid
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.D('product_type_config', ns)
|
|
||||||
debug('delete success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Success",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('Delete failed');
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Delete Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
|
|
||||||
userid = await get_user()
|
|
||||||
if not userid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['operator_id'] = userid
|
|
||||||
ns['userid'] = userid
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
ns['userorgid'] = userorgid
|
|
||||||
|
|
||||||
debug(f'get_product_type_config.dspy:{ns=}')
|
|
||||||
if not ns.get('page'):
|
|
||||||
ns['page'] = 1
|
|
||||||
if not ns.get('sort'):
|
|
||||||
|
|
||||||
|
|
||||||
ns['sort'] = ["created_at desc"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
sql = '''select a.*, b.category_id_text, c.enabled_flg_text
|
|
||||||
from (select * from product_type_config where 1=1 [[filterstr]]) a left join (select id as category_id,
|
|
||||||
name as category_id_text from product_category where has_product='1' AND status='1') b on a.category_id = b.category_id left join (select k as enabled_flg,
|
|
||||||
v as enabled_flg_text from appcodes_kv where id='enabled_flg') c on a.enabled_flg = c.enabled_flg'''
|
|
||||||
|
|
||||||
filterjson = params_kw.get('data_filter')
|
|
||||||
fields_str=r'''[
|
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]'''
|
|
||||||
ori_fields = json.loads(fields_str)
|
|
||||||
if not filterjson:
|
|
||||||
fields = [ f['name'] for f in ori_fields ]
|
|
||||||
filterjson = default_filterjson(fields, ns)
|
|
||||||
filterdic = ns.copy()
|
|
||||||
filterdic['filterstr'] = ''
|
|
||||||
filterdic['userorgid'] = '${userorgid}$'
|
|
||||||
filterdic['userid'] = '${userid}$'
|
|
||||||
if filterjson:
|
|
||||||
dbf = DBFilter(filterjson)
|
|
||||||
conds = dbf.gen(ns)
|
|
||||||
if conds:
|
|
||||||
ns.update(dbf.consts)
|
|
||||||
conds = f' and {conds}'
|
|
||||||
filterdic['filterstr'] = conds
|
|
||||||
ac = ArgsConvert('[[', ']]')
|
|
||||||
vars = ac.findAllVariables(sql)
|
|
||||||
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
|
|
||||||
filterdic.update(NameSpace)
|
|
||||||
sql = ac.convert(sql, filterdic)
|
|
||||||
|
|
||||||
debug(f'{sql=}')
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
r = await sor.sqlPaging(sql, ns)
|
|
||||||
return r
|
|
||||||
return {
|
|
||||||
"total":0,
|
|
||||||
"rows":[]
|
|
||||||
}
|
|
||||||
@ -1,219 +0,0 @@
|
|||||||
|
|
||||||
{
|
|
||||||
"id":"product_type_config_tbl",
|
|
||||||
"widgettype":"Tabular",
|
|
||||||
"options":{
|
|
||||||
"width":"100%",
|
|
||||||
"height":"100%",
|
|
||||||
|
|
||||||
|
|
||||||
"title":"运营商产品类型配置",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"css":"card",
|
|
||||||
|
|
||||||
|
|
||||||
"editable":{
|
|
||||||
|
|
||||||
"new_data_url":"{{entire_url('add_product_type_config.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"delete_data_url":"{{entire_url('delete_product_type_config.dspy')}}",
|
|
||||||
|
|
||||||
|
|
||||||
"update_data_url":"{{entire_url('update_product_type_config.dspy')}}"
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
"data_url":"{{entire_url('./get_product_type_config.dspy')}}",
|
|
||||||
|
|
||||||
"data_method":"GET",
|
|
||||||
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
|
|
||||||
"row_options":{
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"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"
|
|
||||||
],
|
|
||||||
|
|
||||||
"fields":[
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"title": "主键ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "主键ID"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "operator_id",
|
|
||||||
"title": "运营商用户ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "运营商用户ID"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "org_id",
|
|
||||||
"title": "所属机构ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "所属机构ID"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "category_id",
|
|
||||||
"title": "产品类别ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no",
|
|
||||||
"label": "产品类别ID",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "category_id",
|
|
||||||
"textField": "category_id_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "product_category",
|
|
||||||
"tblvalue": "id",
|
|
||||||
"tbltext": "name",
|
|
||||||
"valueField": "category_id",
|
|
||||||
"textField": "category_id_text",
|
|
||||||
"cond": "has_product='1' AND status='1'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('../api/category_options.dspy')}}",
|
|
||||||
"datamethod": "GET"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "config_name",
|
|
||||||
"title": "配置名称",
|
|
||||||
"type": "str",
|
|
||||||
"length": 255,
|
|
||||||
"nullable": "no",
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "配置名称"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "config_json",
|
|
||||||
"title": "配置内容",
|
|
||||||
"type": "text",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "text",
|
|
||||||
"datatype": "text",
|
|
||||||
"label": "配置内容"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "enabled_flg",
|
|
||||||
"title": "是否启用",
|
|
||||||
"type": "char",
|
|
||||||
"length": 1,
|
|
||||||
"default": "1",
|
|
||||||
"label": "是否启用",
|
|
||||||
"uitype": "code",
|
|
||||||
"valueField": "enabled_flg",
|
|
||||||
"textField": "enabled_flg_text",
|
|
||||||
"params": {
|
|
||||||
"dbname": "{{get_module_dbname('product_management')}}",
|
|
||||||
"table": "appcodes_kv",
|
|
||||||
"tblvalue": "k",
|
|
||||||
"tbltext": "v",
|
|
||||||
"valueField": "enabled_flg",
|
|
||||||
"textField": "enabled_flg_text",
|
|
||||||
"cond": "id='enabled_flg'"
|
|
||||||
},
|
|
||||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "启用"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "0",
|
|
||||||
"text": "禁用"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_by",
|
|
||||||
"title": "创建人",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"cwidth": 18,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "str",
|
|
||||||
"label": "创建人"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"title": "创建时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "no",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "datetime",
|
|
||||||
"label": "创建时间"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "updated_at",
|
|
||||||
"title": "更新时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "no",
|
|
||||||
"length": 0,
|
|
||||||
"uitype": "str",
|
|
||||||
"datatype": "datetime",
|
|
||||||
"label": "更新时间"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"page_rows":160,
|
|
||||||
"cache_limit":5
|
|
||||||
}
|
|
||||||
|
|
||||||
,"binds":[]
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
for k,v in ns.items():
|
|
||||||
if v == 'NaN' or v == 'null':
|
|
||||||
ns[k] = None
|
|
||||||
|
|
||||||
userid = await get_user()
|
|
||||||
if not userid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['operator_id'] = userid
|
|
||||||
|
|
||||||
|
|
||||||
userorgid = await get_userorgid()
|
|
||||||
if not userorgid:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Authorization Error",
|
|
||||||
"timeout":3,
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"message":"Please login"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ns['org_id'] = userorgid
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db = DBPools()
|
|
||||||
dbname = get_module_dbname('product_management')
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
|
|
||||||
ns1 = {
|
|
||||||
|
|
||||||
"org_id": userorgid,
|
|
||||||
|
|
||||||
|
|
||||||
"operator_id": userid,
|
|
||||||
|
|
||||||
"id": params_kw.id
|
|
||||||
}
|
|
||||||
recs = await sor.R('product_type_config', ns1)
|
|
||||||
if len(recs) < 1:
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"Record no exist or with wrong ownership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r = await sor.U('product_type_config', ns)
|
|
||||||
debug('update success');
|
|
||||||
return {
|
|
||||||
"widgettype":"Message",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Success",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"ok"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"widgettype":"Error",
|
|
||||||
"options":{
|
|
||||||
"title":"Update Error",
|
|
||||||
"cwidth":16,
|
|
||||||
"cheight":9,
|
|
||||||
"timeout":3,
|
|
||||||
"message":"failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"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