From 4fd136bf5387af8155080247fa8a63c35c3d6198 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 25 May 2026 15:43:52 +0800 Subject: [PATCH] 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 --- .gitignore | 1 + init/data.json | 98 ++--- models/product.json | 67 ++-- models/product_category.json | 44 ++- models/product_type_config.json | 28 +- mysql.ddl.sql | 130 +++++++ product_management.egg-info/PKG-INFO | 7 + product_management.egg-info/SOURCES.txt | 10 + .../dependency_links.txt | 1 + product_management.egg-info/requires.txt | 2 + product_management.egg-info/top_level.txt | 1 + product_management/core.py | 246 +++++++----- wwwroot/api/category_options.dspy | 13 +- wwwroot/api/product_brief.dspy | 48 ++- wwwroot/api/product_category_create.dspy | 31 +- wwwroot/api/product_category_delete.dspy | 37 +- wwwroot/api/product_category_update.dspy | 39 +- wwwroot/api/product_create.dspy | 32 +- wwwroot/api/product_delete.dspy | 18 +- wwwroot/api/product_detail.dspy | 105 +++-- wwwroot/api/product_purchase.dspy | 41 +- wwwroot/api/product_type_config_create.dspy | 19 +- wwwroot/api/product_type_config_delete.dspy | 18 +- wwwroot/api/product_type_config_update.dspy | 26 +- wwwroot/api/product_update.dspy | 45 ++- wwwroot/api/product_use.dspy | 85 +++-- .../delete_product_category.dspy | 47 +++ .../get_product_category.dspy | 16 + wwwroot/product_category_tree/index.ui | 204 ++++++++++ .../new_product_category.dspy | 51 +++ .../update_product_category.dspy | 70 ++++ wwwroot/product_list/add_product.dspy | 51 +++ wwwroot/product_list/delete_product.dspy | 47 +++ wwwroot/product_list/get_product.dspy | 190 +++++++++ wwwroot/product_list/index.ui | 361 ++++++++++++++++++ wwwroot/product_list/update_product.dspy | 70 ++++ .../add_product_type_config.dspy | 65 ++++ .../delete_product_type_config.dspy | 61 +++ .../get_product_type_config.dspy | 148 +++++++ wwwroot/product_type_config_list/index.ui | 219 +++++++++++ .../update_product_type_config.dspy | 86 +++++ 41 files changed, 2443 insertions(+), 435 deletions(-) create mode 100644 .gitignore create mode 100644 mysql.ddl.sql create mode 100644 product_management.egg-info/PKG-INFO create mode 100644 product_management.egg-info/SOURCES.txt create mode 100644 product_management.egg-info/dependency_links.txt create mode 100644 product_management.egg-info/requires.txt create mode 100644 product_management.egg-info/top_level.txt create mode 100644 wwwroot/product_category_tree/delete_product_category.dspy create mode 100644 wwwroot/product_category_tree/get_product_category.dspy create mode 100644 wwwroot/product_category_tree/index.ui create mode 100644 wwwroot/product_category_tree/new_product_category.dspy create mode 100644 wwwroot/product_category_tree/update_product_category.dspy create mode 100644 wwwroot/product_list/add_product.dspy create mode 100644 wwwroot/product_list/delete_product.dspy create mode 100644 wwwroot/product_list/get_product.dspy create mode 100644 wwwroot/product_list/index.ui create mode 100644 wwwroot/product_list/update_product.dspy create mode 100644 wwwroot/product_type_config_list/add_product_type_config.dspy create mode 100644 wwwroot/product_type_config_list/delete_product_type_config.dspy create mode 100644 wwwroot/product_type_config_list/get_product_type_config.dspy create mode 100644 wwwroot/product_type_config_list/index.ui create mode 100644 wwwroot/product_type_config_list/update_product_type_config.dspy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2b4923 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +product_management.egg-info/ diff --git a/init/data.json b/init/data.json index 6b963ac..6a4839a 100644 --- a/init/data.json +++ b/init/data.json @@ -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 中预设全局数据。新机构注册时自动创建根类别。" } diff --git a/models/product.json b/models/product.json index c9e62a3..b3b8aad 100644 --- a/models/product.json +++ b/models/product.json @@ -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'" } ] -} +} \ No newline at end of file diff --git a/models/product_category.json b/models/product_category.json index 29ac89f..5cacaa6 100644 --- a/models/product_category.json +++ b/models/product_category.json @@ -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'" } ] -} +} \ No newline at end of file diff --git a/models/product_type_config.json b/models/product_type_config.json index 383eb90..cb387bf 100644 --- a/models/product_type_config.json +++ b/models/product_type_config.json @@ -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": [ @@ -107,4 +119,4 @@ "cond": "id='enabled_flg'" } ] -} +} \ No newline at end of file diff --git a/mysql.ddl.sql b/mysql.ddl.sql new file mode 100644 index 0000000..ce0e6f6 --- /dev/null +++ b/mysql.ddl.sql @@ -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); + diff --git a/product_management.egg-info/PKG-INFO b/product_management.egg-info/PKG-INFO new file mode 100644 index 0000000..c1d3aec --- /dev/null +++ b/product_management.egg-info/PKG-INFO @@ -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 diff --git a/product_management.egg-info/SOURCES.txt b/product_management.egg-info/SOURCES.txt new file mode 100644 index 0000000..d52c828 --- /dev/null +++ b/product_management.egg-info/SOURCES.txt @@ -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 \ No newline at end of file diff --git a/product_management.egg-info/dependency_links.txt b/product_management.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/product_management.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/product_management.egg-info/requires.txt b/product_management.egg-info/requires.txt new file mode 100644 index 0000000..3f3a3f3 --- /dev/null +++ b/product_management.egg-info/requires.txt @@ -0,0 +1,2 @@ +sqlor +bricks_for_python diff --git a/product_management.egg-info/top_level.txt b/product_management.egg-info/top_level.txt new file mode 100644 index 0000000..5461594 --- /dev/null +++ b/product_management.egg-info/top_level.txt @@ -0,0 +1 @@ +product_management diff --git a/product_management/core.py b/product_management/core.py index df471eb..f1e23b7 100644 --- a/product_management/core.py +++ b/product_management/core.py @@ -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' + 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, {}) + 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,59 +343,57 @@ 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 - return { 'success': True, 'data': { '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, diff --git a/wwwroot/api/category_options.dspy b/wwwroot/api/category_options.dspy index 6703e93..c2481e0 100644 --- a/wwwroot/api/category_options.dspy +++ b/wwwroot/api/category_options.dspy @@ -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' + 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, {}) + 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 diff --git a/wwwroot/api/product_brief.dspy b/wwwroot/api/product_brief.dspy index 1f80410..9c50435 100644 --- a/wwwroot/api/product_brief.dspy +++ b/wwwroot/api/product_brief.dspy @@ -1,57 +1,63 @@ #!/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}$") params['product_id'] = product_id elif product_code: conditions.append("p.product_code = ${product_code}$") params['product_code'] = product_code - + if category_id: conditions.append("p.category_id = ${category_id}$") params['category_id'] = category_id - + where_clause = " AND ".join(conditions) - + sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id, - pc.name as category_name, p.brief_intro, - p.price, p.currency, p.enabled_date, p.expired_date, - p.status, p.product_table_name, p.product_table_id + 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 + 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 [] - - # 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 @@ -61,8 +67,8 @@ try: is_active = False 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 diff --git a/wwwroot/api/product_category_create.dspy b/wwwroot/api/product_category_create.dspy index f7c6f5c..f8a9dc9 100644 --- a/wwwroot/api/product_category_create.dspy +++ b/wwwroot/api/product_category_create.dspy @@ -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 @@ -22,17 +26,24 @@ try: data['status'] = '1' if 'sort_order' not in data: data['sort_order'] = '0' - - # Remove empty product_table_name if has_product is 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_table_name'] = '' - data['product_table_title'] = '' - - fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_table_name', 'product_table_title', 'parent_id')} - + 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: diff --git a/wwwroot/api/product_category_delete.dspy b/wwwroot/api/product_category_delete.dspy index dca700f..acce180 100644 --- a/wwwroot/api/product_category_delete.dspy +++ b/wwwroot/api/product_category_delete.dspy @@ -4,27 +4,46 @@ 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') if not record_id: raise ValueError('Missing id') - + async with DBPools().sqlorContext(dbname) as sor: - # Check if has children - children = await sor.sqlExe("SELECT COUNT(*) as cnt FROM product_category WHERE parent_id = ${id}$", {'id': record_id}) + # 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'}} except Exception as e: diff --git a/wwwroot/api/product_category_update.dspy b/wwwroot/api/product_category_update.dspy index 1081d34..82dcb3f 100644 --- a/wwwroot/api/product_category_update.dspy +++ b/wwwroot/api/product_category_update.dspy @@ -4,26 +4,37 @@ 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') - - data['updated_at'] = now - - # Remove empty product_table_name if has_product is 0 - if data.get('has_product') == '0': - data['product_table_name'] = '' - data['product_table_title'] = '' - - fields = {k: v for k, v in data.items() if v is not None and v != '' or k in ('product_table_name', 'product_table_title', 'parent_id')} - + + # Verify record belongs to current org async with DBPools().sqlorContext(dbname) as sor: - await sor.U('product_category', fields, {'id': record_id}) - + 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: diff --git a/wwwroot/api/product_create.dspy b/wwwroot/api/product_create.dspy index 9373aeb..9c32c22 100644 --- a/wwwroot/api/product_create.dspy +++ b/wwwroot/api/product_create.dspy @@ -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 @@ -24,19 +28,23 @@ try: data['price'] = '0.00' if 'currency' not in data: data['currency'] = 'CNY' - - # If category_id is set but product_table_name is not, look it up from category - if data.get('category_id') and not data.get('product_table_name'): - sql = """SELECT product_table_name FROM product_category - WHERE id = ${category_id}$ AND has_product='1'""" + + # 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) - + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '产品创建成功', 'type': 'success'}} except Exception as e: diff --git a/wwwroot/api/product_delete.dspy b/wwwroot/api/product_delete.dspy index 52a4962..e539b0c 100644 --- a/wwwroot/api/product_delete.dspy +++ b/wwwroot/api/product_delete.dspy @@ -4,15 +4,27 @@ 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') if not record_id: 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'}} except Exception as e: diff --git a/wwwroot/api/product_detail.dspy b/wwwroot/api/product_detail.dspy index 7ea9894..30c9abf 100644 --- a/wwwroot/api/product_detail.dspy +++ b/wwwroot/api/product_detail.dspy @@ -1,105 +1,100 @@ #!/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', '') - + if not product_id and not product_code: result['error'] = '缺少product_id或product_code参数' return json.dumps(result, ensure_ascii=False) - - # Step 1: Get product registry info - conditions = [] - params = {} + + 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, - 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() - config_sql = """SELECT * FROM product_type_config - WHERE category_id = ${category_id}$ + + # 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 diff --git a/wwwroot/api/product_purchase.dspy b/wwwroot/api/product_purchase.dspy index 94c81b1..f99f0c9 100644 --- a/wwwroot/api/product_purchase.dspy +++ b/wwwroot/api/product_purchase.dspy @@ -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,30 +15,31 @@ 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', '') quantity = int(params_kw.get('quantity', '1')) purchase_data = params_kw.get('purchase_data', '{}') - + if not product_id: result['message'] = '缺少product_id参数' return json.dumps(result, ensure_ascii=False) - - # Verify product exists and is active - sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'""" - + + 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 '') @@ -51,8 +50,7 @@ try: if expired and expired < today: result['message'] = '产品已过期' return json.dumps(result, ensure_ascii=False) - - # Create purchase order record + order_id = getID() order_data = { 'id': order_id, @@ -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, @@ -69,14 +68,12 @@ try: 'created_at': now, 'updated_at': now } - - # Check if purchase_orders table exists; if not, create a simple record + try: await sor.C('purchase_orders', order_data) except Exception: - # Table may not exist yet; store as JSON log pass - + result['success'] = True result['order_id'] = order_id result['message'] = '购买请求已提交' diff --git a/wwwroot/api/product_type_config_create.dspy b/wwwroot/api/product_type_config_create.dspy index 98251f6..9588b63 100644 --- a/wwwroot/api/product_type_config_create.dspy +++ b/wwwroot/api/product_type_config_create.dspy @@ -5,29 +5,26 @@ 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) - + result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '配置创建成功', 'type': 'success'}} except Exception as e: diff --git a/wwwroot/api/product_type_config_delete.dspy b/wwwroot/api/product_type_config_delete.dspy index 160da85..09f20a4 100644 --- a/wwwroot/api/product_type_config_delete.dspy +++ b/wwwroot/api/product_type_config_delete.dspy @@ -4,15 +4,27 @@ 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') if not record_id: 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'}} except Exception as e: diff --git a/wwwroot/api/product_type_config_update.dspy b/wwwroot/api/product_type_config_update.dspy index e48440b..9613798 100644 --- a/wwwroot/api/product_type_config_update.dspy +++ b/wwwroot/api/product_type_config_update.dspy @@ -4,19 +4,31 @@ 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') - - data['updated_at'] = now - + async with DBPools().sqlorContext(dbname) as sor: - await sor.U('product_type_config', data, {'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('无权操作该记录') + + 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: diff --git a/wwwroot/api/product_update.dspy b/wwwroot/api/product_update.dspy index ba219ba..fb63822 100644 --- a/wwwroot/api/product_update.dspy +++ b/wwwroot/api/product_update.dspy @@ -4,28 +4,41 @@ 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') - - 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'] - + + # Verify belongs to org async with DBPools().sqlorContext(dbname) as sor: - await sor.U('product', data, {'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('无权操作该记录') + + 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: diff --git a/wwwroot/api/product_use.dspy b/wwwroot/api/product_use.dspy index ca3a69f..e20ca9c 100644 --- a/wwwroot/api/product_use.dspy +++ b/wwwroot/api/product_use.dspy @@ -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,56 +15,58 @@ 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', '') order_id = params_kw.get('order_id', '') use_data = params_kw.get('use_data', '{}') - + if not product_id: result['message'] = '缺少product_id参数' return json.dumps(result, ensure_ascii=False) - - # Step 1: Verify product exists and is active - sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1'""" - + + 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 - - # Step 4: Create use record + + # 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 @@ -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'] = '产品使用成功' diff --git a/wwwroot/product_category_tree/delete_product_category.dspy b/wwwroot/product_category_tree/delete_product_category.dspy new file mode 100644 index 0000000..bce561b --- /dev/null +++ b/wwwroot/product_category_tree/delete_product_category.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_category_tree/get_product_category.dspy b/wwwroot/product_category_tree/get_product_category.dspy new file mode 100644 index 0000000..235b1d6 --- /dev/null +++ b/wwwroot/product_category_tree/get_product_category.dspy @@ -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 [] \ No newline at end of file diff --git a/wwwroot/product_category_tree/index.ui b/wwwroot/product_category_tree/index.ui new file mode 100644 index 0000000..018fe81 --- /dev/null +++ b/wwwroot/product_category_tree/index.ui @@ -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')}}" + } + } +] + +} \ No newline at end of file diff --git a/wwwroot/product_category_tree/new_product_category.dspy b/wwwroot/product_category_tree/new_product_category.dspy new file mode 100644 index 0000000..1ba8030 --- /dev/null +++ b/wwwroot/product_category_tree/new_product_category.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_category_tree/update_product_category.dspy b/wwwroot/product_category_tree/update_product_category.dspy new file mode 100644 index 0000000..701f74e --- /dev/null +++ b/wwwroot/product_category_tree/update_product_category.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_list/add_product.dspy b/wwwroot/product_list/add_product.dspy new file mode 100644 index 0000000..62a21df --- /dev/null +++ b/wwwroot/product_list/add_product.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_list/delete_product.dspy b/wwwroot/product_list/delete_product.dspy new file mode 100644 index 0000000..f322e45 --- /dev/null +++ b/wwwroot/product_list/delete_product.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_list/get_product.dspy b/wwwroot/product_list/get_product.dspy new file mode 100644 index 0000000..d7ceaa6 --- /dev/null +++ b/wwwroot/product_list/get_product.dspy @@ -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":[] +} \ No newline at end of file diff --git a/wwwroot/product_list/index.ui b/wwwroot/product_list/index.ui new file mode 100644 index 0000000..feee501 --- /dev/null +++ b/wwwroot/product_list/index.ui @@ -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":[] + +} \ No newline at end of file diff --git a/wwwroot/product_list/update_product.dspy b/wwwroot/product_list/update_product.dspy new file mode 100644 index 0000000..b20da94 --- /dev/null +++ b/wwwroot/product_list/update_product.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_type_config_list/add_product_type_config.dspy b/wwwroot/product_type_config_list/add_product_type_config.dspy new file mode 100644 index 0000000..374b50e --- /dev/null +++ b/wwwroot/product_type_config_list/add_product_type_config.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_type_config_list/delete_product_type_config.dspy b/wwwroot/product_type_config_list/delete_product_type_config.dspy new file mode 100644 index 0000000..813a4d3 --- /dev/null +++ b/wwwroot/product_type_config_list/delete_product_type_config.dspy @@ -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" + } +} \ No newline at end of file diff --git a/wwwroot/product_type_config_list/get_product_type_config.dspy b/wwwroot/product_type_config_list/get_product_type_config.dspy new file mode 100644 index 0000000..6df93a9 --- /dev/null +++ b/wwwroot/product_type_config_list/get_product_type_config.dspy @@ -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":[] +} \ No newline at end of file diff --git a/wwwroot/product_type_config_list/index.ui b/wwwroot/product_type_config_list/index.ui new file mode 100644 index 0000000..23820c2 --- /dev/null +++ b/wwwroot/product_type_config_list/index.ui @@ -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":[] + +} \ No newline at end of file diff --git a/wwwroot/product_type_config_list/update_product_type_config.dspy b/wwwroot/product_type_config_list/update_product_type_config.dspy new file mode 100644 index 0000000..be5984a --- /dev/null +++ b/wwwroot/product_type_config_list/update_product_type_config.dspy @@ -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" + } +} \ No newline at end of file