refactor: reseller org_id isolation for product_management module

- product_category: org_id scoped tree, product_table_name -> product_type
- product: org_id scoped, added extra_json for custom attributes, product_type field
- product_type_config: org_id + operator_id dual isolation, unique key on (org_id, operator_id, category_id, config_name)
- All 18 API endpoints enforce org_id filtering via ServerEnv
- core.py: all methods accept optional org_id, default to current user's org
- CRUD definitions: logined_userorgid set to org_id on all lists
- init/data.json: removed hardcoded global categories (managed per reseller)
- Rebuilt mysql.ddl.sql and all CRUD UI files
This commit is contained in:
yumoqing 2026-05-25 15:43:52 +08:00
parent 6b37adc92d
commit 4fd136bf53
41 changed files with 2443 additions and 435 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
product_management.egg-info/

View File

@ -1,68 +1,50 @@
{
"product_category": [
"appcodes": [
{
"id": "pc-root-001",
"parent_id": "0",
"name": "产品中心",
"description": "所有产品根类别",
"has_product": "0",
"product_table_name": "",
"product_table_title": "",
"sort_order": 0,
"icon": "📦",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
"id": "product_status",
"name": "产品状态",
"hierarchy_flg": "0"
},
{
"id": "pc-telecom-001",
"parent_id": "pc-root-001",
"name": "通信服务",
"description": "电信运营商相关产品",
"has_product": "1",
"product_table_name": "telecom_products",
"product_table_title": "通信产品表",
"sort_order": 1,
"icon": "📡",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
"id": "product_price_type",
"name": "产品价格类型",
"hierarchy_flg": "0"
},
{
"id": "pc-cloud-001",
"parent_id": "pc-root-001",
"name": "云服务",
"description": "云计算相关产品",
"has_product": "1",
"product_table_name": "cloud_products",
"product_table_title": "云产品表",
"sort_order": 2,
"icon": "☁️",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
"id": "product_category_status",
"name": "产品类别状态",
"hierarchy_flg": "0"
},
{
"id": "pc-data-001",
"parent_id": "pc-root-001",
"name": "数据服务",
"description": "数据处理与分析相关产品",
"has_product": "1",
"product_table_name": "data_products",
"product_table_title": "数据产品表",
"sort_order": 3,
"icon": "📊",
"status": "1",
"org_id": "0",
"created_by": "system",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00"
"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 中预设全局数据。新机构注册时自动创建根类别。"
}

View File

@ -3,7 +3,9 @@
{
"name": "product",
"title": "产品注册表",
"primary": ["id"],
"primary": [
"id"
],
"catelog": "entity"
}
],
@ -37,17 +39,10 @@
"nullable": "no"
},
{
"name": "product_table_name",
"title": "产品数据表名",
"name": "product_type",
"title": "产品类型标识",
"type": "str",
"length": 255,
"nullable": "no"
},
{
"name": "product_table_id",
"title": "产品数据表记录ID",
"type": "str",
"length": 32,
"length": 64,
"nullable": "no"
},
{
@ -60,6 +55,11 @@
"title": "产品详情",
"type": "text"
},
{
"name": "extra_json",
"title": "扩展属性",
"type": "text"
},
{
"name": "enabled_date",
"title": "启用日期",
@ -133,29 +133,43 @@
],
"indexes": [
{
"name": "idx_product_category",
"name": "idx_product_org_category",
"idxtype": "index",
"idxfields": ["category_id"]
"idxfields": [
"org_id",
"category_id"
]
},
{
"name": "idx_product_code",
"name": "idx_product_org_code",
"idxtype": "unique",
"idxfields": ["product_code"]
"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_org",
"idxtype": "index",
"idxfields": ["org_id"]
"idxfields": [
"status"
]
},
{
"name": "idx_product_enabled_expired",
"idxtype": "index",
"idxfields": ["enabled_date", "expired_date"]
"idxfields": [
"enabled_date",
"expired_date"
]
}
],
"codes": [
@ -179,6 +193,13 @@
"valuefield": "k",
"textfield": "v",
"cond": "id='product_price_type'"
},
{
"field": "product_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='product_type'"
}
]
}

View File

@ -3,7 +3,9 @@
{
"name": "product_category",
"title": "产品类别树",
"primary": ["id"],
"primary": [
"id"
],
"catelog": "entity"
}
],
@ -42,14 +44,14 @@
"default": "0"
},
{
"name": "product_table_name",
"title": "产品数据表名",
"name": "product_type",
"title": "产品类型标识",
"type": "str",
"length": 255
"length": 64
},
{
"name": "product_table_title",
"title": "产品数据表显示名",
"name": "product_type_title",
"title": "产品类型显示名",
"type": "str",
"length": 255
},
@ -100,19 +102,28 @@
],
"indexes": [
{
"name": "idx_product_category_parent",
"name": "idx_pc_org_parent",
"idxtype": "index",
"idxfields": ["parent_id"]
"idxfields": [
"org_id",
"parent_id"
]
},
{
"name": "idx_product_category_status",
"name": "idx_pc_org_status",
"idxtype": "index",
"idxfields": ["status"]
"idxfields": [
"org_id",
"status"
]
},
{
"name": "idx_product_category_org",
"name": "idx_pc_org_type",
"idxtype": "index",
"idxfields": ["org_id"]
"idxfields": [
"org_id",
"product_type"
]
}
],
"codes": [
@ -136,6 +147,13 @@
"valuefield": "k",
"textfield": "v",
"cond": "id='product_category_status'"
},
{
"field": "product_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "id='product_type'"
}
]
}

View File

@ -3,7 +3,9 @@
{
"name": "product_type_config",
"title": "运营商产品类型配置",
"primary": ["id"],
"primary": [
"id"
],
"catelog": "relation"
}
],
@ -76,19 +78,29 @@
],
"indexes": [
{
"name": "idx_ptc_operator",
"name": "idx_ptc_org_opr",
"idxtype": "index",
"idxfields": ["operator_id"]
"idxfields": [
"org_id",
"operator_id"
]
},
{
"name": "idx_ptc_org_category",
"idxtype": "index",
"idxfields": ["org_id", "category_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"]
"idxfields": [
"enabled_flg"
]
}
],
"codes": [

130
mysql.ddl.sql Normal file
View File

@ -0,0 +1,130 @@
-- ./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);

View File

@ -0,0 +1,7 @@
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

View File

@ -0,0 +1,10 @@
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

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
sqlor
bricks_for_python

View File

@ -0,0 +1 @@
product_management

View File

@ -1,4 +1,4 @@
"""Product Management Core Business Logic"""
"""Product Management Core Business Logic - org_id isolated per reseller"""
import json
import time
import datetime
@ -9,7 +9,11 @@ from ahserver.serverenv import ServerEnv
class ProductManager:
"""Core manager for product catalog, category tree, and operator configs."""
"""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_db(self):
"""Get database context following Sage singleton fork-safe pattern."""
@ -21,14 +25,22 @@ class ProductManager:
db.databases = config.databases
return db, dbname
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 as nested dict."""
"""Get full category tree for a specific org (reseller)."""
if not org_id:
org_id = self._get_current_org_id()
db, dbname = self._get_db()
async with db.sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_category
WHERE status = '1'
WHERE org_id = ${org_id}$ AND status = '1'
ORDER BY sort_order ASC, name ASC"""
rows = await sor.sqlExe(sql, {})
rows = await sor.sqlExe(sql, {'org_id': org_id})
rows = rows or []
# Build tree
@ -48,35 +60,48 @@ class ProductManager:
return {'success': True, 'tree': tree}
async def get_products_by_category(self, category_id, status='1'):
"""Get all active products under a category (including sub-categories)."""
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()
db, dbname = self._get_db()
# Get all sub-category IDs recursively
# Get all sub-category IDs recursively within same org
async with db.sqlorContext(dbname) as sor:
all_ids = [category_id]
queue = [category_id]
while queue:
parent = queue.pop(0)
children = await sor.sqlExe(
"SELECT id FROM product_category WHERE parent_id = ${pid}$",
{'pid': parent}
"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)
placeholders = ','.join([f'${i}$' for i in range(len(all_ids))])
params = {str(i): v for i, v in enumerate(all_ids)}
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
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.status = '${len(all_ids)}$'
AND p.org_id = ${org_id}$
AND p.status = ${status}$
ORDER BY p.sort_order ASC, p.created_at DESC"""
params[str(len(all_ids))] = status
params['status'] = status
rows = await sor.sqlExe(sql, params)
@ -91,16 +116,26 @@ class ProductManager:
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):
"""Get product brief via standardized interface."""
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()
db, dbname = self._get_db()
conditions = ["p.status = '1'"]
params = {}
conditions = ["p.status = '1'", "p.org_id = ${org_id}$"]
params = {'org_id': org_id}
if product_id:
conditions.append("p.id = ${product_id}$")
@ -119,9 +154,9 @@ class ProductManager:
sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id,
pc.name as category_name, p.brief_intro,
p.price, p.currency, p.enabled_date, p.expired_date,
p.status, p.product_table_name, p.product_table_id
p.status, p.product_type, p.extra_json
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
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)
@ -141,8 +176,14 @@ class ProductManager:
return {'success': True, 'data': result, 'total': len(result)}
async def get_product_detail(self, product_id=None, product_code=None, user_id=None):
"""Get product detail via standardized interface."""
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:
@ -152,8 +193,8 @@ class ProductManager:
db, dbname = self._get_db()
conditions = []
params = {}
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
@ -167,30 +208,37 @@ class ProductManager:
where_clause = " AND ".join(conditions)
async with db.sqlorContext(dbname) as sor:
# Get product + category info
sql = f"""SELECT p.*, pc.name as category_name, pc.description as category_description
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
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'}
return {'success': False, 'error': 'Product not found or no access'}
product_info = dict(rows[0])
# Get operator config
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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 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'],
'user_id': user_id,
'org_id': org_id
'org_id': org_id,
'user_id': user_id
})
operator_config = {}
@ -203,20 +251,6 @@ class ProductManager:
except:
operator_config['config_parsed'] = {}
# Try to fetch actual product data
actual_product_data = None
table_name = product_info.get('product_table_name', '')
table_id = product_info.get('product_table_id', '')
if table_name and table_id:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except Exception as e:
actual_product_data = {'_fetch_error': str(e)}
return {
'success': True,
'data': {
@ -225,13 +259,14 @@ class ProductManager:
'name': product_info.get('category_name'),
'description': product_info.get('category_description')
},
'operator_config': operator_config,
'actual_product_data': actual_product_data
'operator_config': operator_config
}
}
async def purchase_product(self, product_id, quantity=1, purchase_data=None, user_id=None):
"""Purchase a product via standardized interface."""
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:
@ -247,15 +282,13 @@ class ProductManager:
quantity = int(quantity) if quantity else 1
async with db.sqlorContext(dbname) as sor:
# Verify product
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
rows = await sor.sqlExe(sql, {'product_id': product_id})
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 disabled'}
return {'success': False, 'message': 'Product not found or no access'}
product = dict(rows[0])
# Check date range
today = datetime.date.today().isoformat()
enabled = str(product.get('enabled_date', '') or '')
expired = str(product.get('expired_date', '') or '')
@ -264,7 +297,6 @@ class ProductManager:
if expired and expired < today:
return {'success': False, 'message': 'Product has expired'}
# Create purchase order
order_id = getID()
order_data = {
'id': order_id,
@ -272,6 +304,7 @@ class ProductManager:
'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,
@ -285,8 +318,7 @@ class ProductManager:
try:
await sor.C('purchase_orders', order_data)
except Exception:
# Table might not exist yet
info(f"Purchase order {order_id} created (table purchase_orders not available)")
pass
return {
'success': True,
@ -294,8 +326,10 @@ class ProductManager:
'message': 'Purchase request submitted'
}
async def use_product(self, product_id, order_id=None, use_data=None, user_id=None):
"""Use a product via standardized interface."""
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:
@ -309,41 +343,36 @@ class ProductManager:
db, dbname = self._get_db()
async with db.sqlorContext(dbname) as sor:
# Verify product
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
rows = await sor.sqlExe(sql, {'product_id': product_id})
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 disabled'}
return {'success': False, 'message': 'Product not found or no access'}
product = dict(rows[0])
# Verify purchase (if purchase_orders table exists)
# 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
'user_id': user_id,
'org_id': org_id
})
if not purchases and not order_id:
return {'success': False, 'message': 'Product not purchased'}
except:
# purchase_orders table may not exist
pass
# Fetch actual product data
actual_product_data = None
table_name = product.get('product_table_name', '')
table_id = product.get('product_table_id', '')
if table_name and table_id:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except:
pass
@ -353,15 +382,18 @@ class ProductManager:
'product_info': {
'id': product['id'],
'name': product['product_name'],
'code': product['product_code']
'code': product['product_code'],
'product_type': product.get('product_type', '')
},
'actual_data': actual_product_data
'extra_parsed': extra_parsed
},
'message': 'Product use successful'
}
async def get_operator_config(self, category_id, user_id=None):
"""Get operator configuration for a category."""
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:
@ -370,19 +402,18 @@ class ProductManager:
user_id = 'anonymous'
db, dbname = self._get_db()
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
async with db.sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_type_config
WHERE category_id = ${category_id}$
AND org_id = ${org_id}$
AND enabled_flg = '1'
AND (operator_id = ${user_id}$ OR org_id = ${org_id}$)
AND (operator_id = ${user_id}$ OR operator_id = '0')
ORDER BY created_at DESC"""
rows = await sor.sqlExe(sql, {
'category_id': category_id,
'user_id': user_id,
'org_id': org_id
'org_id': org_id,
'user_id': user_id
})
configs = []
@ -398,8 +429,10 @@ class ProductManager:
return {'success': True, 'configs': configs}
async def set_operator_config(self, category_id, config_name, config_json, user_id=None, org_id=None):
"""Create or update operator configuration."""
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:
@ -407,39 +440,44 @@ class ProductManager:
except:
return {'success': False, 'message': 'User not authenticated'}
if not org_id:
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
if not config_name:
return {'success': False, 'message': 'Missing config_name'}
db, dbname = self._get_db()
now = time.strftime('%Y-%m-%d %H:%M:%S')
# Validate config_json is valid JSON
try:
json.loads(config_json)
except:
return {'success': False, 'message': 'Invalid config_json format'}
async with db.sqlorContext(dbname) as sor:
# Check if config exists for this user+category
# 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, 'user_id': user_id, 'config_name': config_name}
{'category_id': category_id, 'org_id': org_id,
'user_id': user_id, 'config_name': config_name}
)
if existing:
# Update
config_id = existing[0]['id']
await sor.U('product_type_config', {
'config_json': config_json,
'updated_at': now
}, {'id': config_id})
}, {'id': config_id, 'org_id': org_id})
return {'success': True, 'id': config_id, 'message': 'Config updated'}
else:
# Create
config_id = getID()
await sor.C('product_type_config', {
'id': config_id,

View File

@ -4,13 +4,18 @@ import json
result = {'success': False, 'data': []}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
dbname = get_module_dbname('product_management')
sql = """SELECT id, name FROM product_category
WHERE has_product='1' AND status='1'
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, {})
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

View File

@ -1,25 +1,31 @@
#!/usr/bin/env python3
"""
产品简介标准化接口
产品简介标准化接口 (按机构隔离)
参数:
product_id: 产品ID (product表id)
product_code: 产品编码 (可选与product_id二选一)
category_id: 类别ID (可选,返回该类别下所有产品的简介)
product_code: 产品编码 (可选)
category_id: 类别ID (可选,返回该类别下所有产品)
org_id: 机构ID (可选,不传则用当前用户机构)
返回:
{success, data: [{id, product_code, product_name, category_name, brief_intro, price, currency, enabled_date, expired_date, status}]}
{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:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')
category_id = params_kw.get('category_id', '')
conditions = ["p.status = '1'"]
params = {}
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}$")
@ -37,9 +43,9 @@ try:
sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id,
pc.name as category_name, p.brief_intro,
p.price, p.currency, p.enabled_date, p.expired_date,
p.status, p.product_table_name, p.product_table_id
p.status, p.product_type, p.extra_json
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
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"""
@ -47,11 +53,11 @@ try:
rows = await sor.sqlExe(sql, params)
rows = rows or []
# Check enabled/expired date
import datetime
today = datetime.date.today().isoformat()
active_products = []
for r in rows:
r = dict(r)
enabled = str(r.get('enabled_date', '') or '')
expired = str(r.get('expired_date', '') or '')
is_active = True
@ -62,7 +68,7 @@ try:
r['is_active'] = is_active
active_products.append(r)
result['data'] = [dict(r) for r in active_products]
result['data'] = active_products
result['total'] = len(result['data'])
result['success'] = True

View File

@ -5,12 +5,16 @@ from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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
@ -23,12 +27,19 @@ try:
if 'sort_order' not in data:
data['sort_order'] = '0'
# Remove empty product_table_name if has_product is 0
if data.get('has_product') == '0':
data['product_table_name'] = ''
data['product_table_title'] = ''
# 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('父类别不存在或不属于当前机构')
fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_table_name', 'product_table_title', 'parent_id')}
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)

View File

@ -4,6 +4,11 @@ import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
@ -11,19 +16,33 @@ try:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
# Check if has children
children = await sor.sqlExe("SELECT COUNT(*) as cnt FROM product_category WHERE parent_id = ${id}$", {'id': record_id})
# 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 if has products
products = await sor.sqlExe("SELECT COUNT(*) as cnt FROM product WHERE category_id = ${id}$", {'id': record_id})
# 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})
await sor.D('product_category', {'id': record_id, 'org_id': org_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '类别删除成功', 'type': 'success'}}

View File

@ -4,25 +4,36 @@ import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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
# Remove empty product_table_name if has_product is 0
if data.get('has_product') == '0':
data['product_table_name'] = ''
data['product_table_title'] = ''
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_table_name', 'product_table_title', 'parent_id')}
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})
await sor.U('product_category', fields, {'id': record_id, 'org_id': org_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '类别更新成功', 'type': 'success'}}

View File

@ -5,12 +5,16 @@ from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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
@ -25,14 +29,18 @@ try:
if 'currency' not in data:
data['currency'] = 'CNY'
# If category_id is set but product_table_name is not, look it up from category
if data.get('category_id') and not data.get('product_table_name'):
sql = """SELECT product_table_name FROM product_category
WHERE id = ${category_id}$ AND has_product='1'"""
# Verify category belongs to current org
if data.get('category_id'):
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'category_id': data['category_id']})
if rows:
data['product_table_name'] = rows[0]['product_table_name']
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)

View File

@ -4,6 +4,11 @@ import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
@ -11,7 +16,14 @@ try:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
await sor.D('product', {'id': record_id})
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'}}

View File

@ -1,21 +1,27 @@
#!/usr/bin/env python3
"""
产品详情标准化接口
产品详情标准化接口 (按机构隔离)
参数:
product_id: 产品ID (product表id)
product_code: 产品编码 (可选与product_id二选一)
product_id: 产品ID
product_code: 产品编码 (可选)
org_id: 机构ID (可选,不传则用当前用户机构)
返回:
{success, data: {product_info, category_info, detail_config, actual_product_data}}
{success, data: {product_info, category_info, operator_config, extra_parsed}}
说明:
通过product.product_table_name和product.product_table_id
动态查询实际产品数据表中的详细信息
product_info 包含产品全部信息
extra_parsed 是 extra_json 解析后的结构化数据
operator_config 是当前运营商对该产品类型的配置
"""
import json
result = {'success': False, 'data': {}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')
@ -23,9 +29,10 @@ try:
result['error'] = '缺少product_id或product_code参数'
return json.dumps(result, ensure_ascii=False)
# Step 1: Get product registry info
conditions = []
params = {}
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
@ -35,71 +42,59 @@ try:
where_clause = " AND ".join(conditions)
sql = f"""SELECT p.*, pc.name as category_name, pc.description as category_description,
pc.product_table_name as category_table_name
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
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'] = '产品不存在'
result['error'] = '产品不存在或无权访问'
return json.dumps(result, ensure_ascii=False)
product_info = dict(rows[0])
# Step 2: Get operator config for this category
user_id = await get_user()
# 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 (operator_id = ${user_id}$ OR org_id = ${org_id}$)
AND org_id = ${org_id}$
AND (operator_id = ${user_id}$ OR operator_id = '0')
ORDER BY created_at DESC LIMIT 1"""
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
config_rows = await sor.sqlExe(config_sql, {
'category_id': product_info['category_id'],
'user_id': user_id,
'org_id': org_id
'org_id': org_id,
'user_id': user_id
})
operator_config = {}
if config_rows:
operator_config = dict(config_rows[0])
if operator_config.get('config_json'):
import ast
config_json = operator_config.get('config_json', '')
if config_json:
try:
operator_config['config_parsed'] = json.loads(operator_config['config_json'])
operator_config['config_parsed'] = json.loads(config_json)
except:
operator_config['config_parsed'] = {}
# Step 3: Try to fetch actual product data from the product_table
actual_product_data = None
table_name = product_info.get('product_table_name', '')
table_id = product_info.get('product_table_id', '')
if table_name and table_id:
try:
# Use dynamic table query
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
except Exception as table_err:
actual_product_data = {'_error': f'无法获取实际产品数据: {str(table_err)}'}
result['data'] = {
'product_info': product_info,
'category_info': {
'name': product_info.get('category_name'),
'description': product_info.get('category_description')
},
'operator_config': operator_config,
'actual_product_data': actual_product_data
'operator_config': operator_config
}
result['success'] = True

View File

@ -1,15 +1,13 @@
#!/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
@ -17,8 +15,10 @@ from appPublic.uniqueID import getID
result = {'success': False, 'order_id': '', 'message': ''}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')
@ -29,18 +29,17 @@ try:
result['message'] = '缺少product_id参数'
return json.dumps(result, ensure_ascii=False)
# Verify product exists and is active
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
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})
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])
# Check enabled/expired dates
import datetime
today = datetime.date.today().isoformat()
enabled = str(product.get('enabled_date', '') or '')
@ -52,7 +51,6 @@ try:
result['message'] = '产品已过期'
return json.dumps(result, ensure_ascii=False)
# Create purchase order record
order_id = getID()
order_data = {
'id': order_id,
@ -60,8 +58,9 @@ try:
'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': product.get('price', 0),
'unit_price': float(product.get('price', 0)),
'total_price': float(product.get('price', 0)) * quantity,
'currency': product.get('currency', 'CNY'),
'purchase_data': purchase_data,
@ -70,11 +69,9 @@ try:
'updated_at': now
}
# Check if purchase_orders table exists; if not, create a simple record
try:
await sor.C('purchase_orders', order_data)
except Exception:
# Table may not exist yet; store as JSON log
pass
result['success'] = True

View File

@ -5,26 +5,23 @@ from appPublic.uniqueID import getID
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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'
# Auto-fill org_id if not provided
if 'org_id' not in data or not data['org_id']:
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
data['org_id'] = org_id
async with DBPools().sqlorContext(dbname) as sor:
await sor.C('product_type_config', data)

View File

@ -4,6 +4,11 @@ import json
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
dbname = get_module_dbname('product_management')
data = dict(params_kw)
record_id = data.get('id')
@ -11,7 +16,14 @@ try:
raise ValueError('Missing id')
async with DBPools().sqlorContext(dbname) as sor:
await sor.D('product_type_config', {'id': record_id})
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'}}

View File

@ -4,18 +4,30 @@ import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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})
await sor.U('product_type_config', data, {'id': record_id, 'org_id': org_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '配置更新成功', 'type': 'success'}}

View File

@ -4,27 +4,40 @@ import json, time
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error'}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '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_id changed, update product_table_name from new category
if data.get('category_id') and not data.get('product_table_name'):
sql = """SELECT product_table_name FROM product_category
WHERE id = ${category_id}$ AND has_product='1'"""
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(sql, {'category_id': data['category_id']})
if rows:
data['product_table_name'] = rows[0]['product_table_name']
# 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})
await sor.U('product', data, {'id': record_id, 'org_id': org_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '产品更新成功', 'type': 'success'}}

View File

@ -1,15 +1,13 @@
#!/usr/bin/env python3
"""
产品使用标准化接口
产品使用标准化接口 (按机构隔离)
参数:
product_id: 产品ID
order_id: 订单ID (可选)
use_data: 使用附加数据 (JSON字符串)
org_id: 机构ID (可选)
返回:
{success, use_record_id, data}
说明:
验证用户是否拥有该产品后,执行使用操作
动态路由到实际产品表的使用逻辑
{success, use_record_id, data: {product_info, extra_parsed}}
"""
import json, time
from appPublic.uniqueID import getID
@ -17,8 +15,10 @@ from appPublic.uniqueID import getID
result = {'success': False, 'use_record_id': '', 'message': '', 'data': {}}
try:
dbname = get_module_dbname('product_management')
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')
@ -29,44 +29,44 @@ try:
result['message'] = '缺少product_id参数'
return json.dumps(result, ensure_ascii=False)
# Step 1: Verify product exists and is active
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'"""
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})
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
if not rows:
result['message'] = '产品不存在或已禁用'
result['message'] = '产品不存在或无权访问'
return json.dumps(result, ensure_ascii=False)
product = dict(rows[0])
# Step 2: Verify user has purchased this product
# Check purchase_orders table
purchase_sql = """SELECT * FROM purchase_orders
WHERE product_id = ${product_id}$
AND buyer_id = ${user_id}$
AND status IN ('active', 'pending')"""
purchases = await sor.sqlExe(purchase_sql, {'product_id': product_id, 'user_id': user_id})
if not purchases and not order_id:
result['message'] = '您尚未购买此产品'
return json.dumps(result, ensure_ascii=False)
# Step 3: Try to fetch actual product data
actual_product_data = None
table_name = product.get('product_table_name', '')
table_id = product.get('product_table_id', '')
if table_name and table_id:
# Parse extra_json for the product
extra_parsed = {}
extra_str = product.get('extra_json', '')
if extra_str:
try:
detail_sql = f"SELECT * FROM {table_name} WHERE id = ${{table_id}}$"
detail_rows = await sor.sqlExe(detail_sql, {'table_id': table_id})
if detail_rows:
actual_product_data = dict(detail_rows[0])
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
# Step 4: Create use record
use_record_id = getID()
result['success'] = True
result['use_record_id'] = use_record_id
@ -74,9 +74,10 @@ try:
'product_info': {
'id': product['id'],
'name': product['product_name'],
'code': product['product_code']
'code': product['product_code'],
'product_type': product.get('product_type', '')
},
'actual_data': actual_product_data
'extra_parsed': extra_parsed
}
result['message'] = '产品使用成功'

View File

@ -0,0 +1,47 @@
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"
}
}

View File

@ -0,0 +1,16 @@
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 []

View File

@ -0,0 +1,204 @@
{
"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')}}"
}
}
]
}

View File

@ -0,0 +1,51 @@
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"
}
}

View File

@ -0,0 +1,70 @@
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"
}
}

View File

@ -0,0 +1,51 @@
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"
}
}

View File

@ -0,0 +1,47 @@
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"
}
}

View File

@ -0,0 +1,190 @@
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":[]
}

View File

@ -0,0 +1,361 @@
{
"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":[]
}

View File

@ -0,0 +1,70 @@
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"
}
}

View File

@ -0,0 +1,65 @@
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"
}
}

View File

@ -0,0 +1,61 @@
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"
}
}

View File

@ -0,0 +1,148 @@
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":[]
}

View File

@ -0,0 +1,219 @@
{
"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":[]
}

View File

@ -0,0 +1,86 @@
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"
}
}