diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..b108a82 --- /dev/null +++ b/init/data.json @@ -0,0 +1,11 @@ +{ + "distributor_status": { + "active": "活跃", + "suspended": "暂停", + "terminated": "终止" + }, + "markup_type": { + "fixed": "固定加价", + "percentage": "百分比加价" + } +} diff --git a/json/distributor_pipeline.json b/json/distributor_pipeline.json new file mode 100644 index 0000000..c0a5a65 --- /dev/null +++ b/json/distributor_pipeline.json @@ -0,0 +1,33 @@ +{ + "tblname": "distributor_pipeline", + "title": "分销商产线配置", + "params": { + "sortby": "created_at", + "browserfields": { + "exclouded": ["id"], + "cwidth": {}, + "alters": { + "distributor_id": {"dataurl": "{{entire_url('../api/get_search_distributor_id.dspy')}}"}, + "pipeline_id": {"dataurl": "{{entire_url('../api/get_search_pipeline_id.dspy')}}"} + } + }, + "editexclouded": [ + "id", "created_at", "updated_at" + ], + "editable": { + "new_data_url": "{{entire_url('../api/distributor_pipeline_create.dspy')}}", + "update_data_url": "{{entire_url('../api/distributor_pipeline_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/distributor_pipeline_delete.dspy')}}" + }, + "confidential_fields": [], + "subtables": [ + {"field": "distributor_id", "title": "分销商", "subtable": "distributors"}, + {"field": "pipeline_id", "title": "产线", "subtable": "pipelines"} + ], + "data_filter": { + "distributor_id": "", + "pipeline_id": "", + "status": "" + } + } +} diff --git a/json/distributors.json b/json/distributors.json new file mode 100644 index 0000000..5162c31 --- /dev/null +++ b/json/distributors.json @@ -0,0 +1,24 @@ +{ + "tblname": "distributors", + "title": "分销商管理", + "params": { + "sortby": "created_at", + "browserfields": { + "exclouded": ["id", "api_key", "remarks"], + "cwidth": {} + }, + "editexclouded": [ + "id", "created_at", "updated_at" + ], + "editable": { + "new_data_url": "{{entire_url('../api/distributors_create.dspy')}}", + "update_data_url": "{{entire_url('../api/distributors_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/distributors_delete.dspy')}}" + }, + "confidential_fields": ["api_key"], + "data_filter": { + "name": "", + "status": "" + } + } +} diff --git a/models/distributor_pipeline.json b/models/distributor_pipeline.json new file mode 100644 index 0000000..7c3443e --- /dev/null +++ b/models/distributor_pipeline.json @@ -0,0 +1,26 @@ +{ + "summary": [{"name": "distributor_pipeline", "title": "分销商-产线关联定价表", "primary": ["id"]}], + "fields": [ + {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"}, + {"name": "distributor_id", "title": "分销商ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "pipeline_id", "title": "产线ID", "type": "str", "length": 32, "nullable": "no"}, + {"name": "custom_price", "title": "自定义单价", "type": "double", "length": 15, "dec": 4}, + {"name": "markup_type", "title": "加价方式", "type": "str", "length": 20}, + {"name": "markup_value", "title": "加价值", "type": "double", "length": 10, "dec": 2}, + {"name": "daily_limit", "title": "日限额", "type": "int", "default": "0"}, + {"name": "monthly_limit", "title": "月限额", "type": "int", "default": "0"}, + {"name": "today_usage", "title": "今日已用", "type": "int", "default": "0"}, + {"name": "month_usage", "title": "本月已用", "type": "int", "default": "0"}, + {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp"} + ], + "indexes": [ + {"name": "idx_dp_dist_pipe", "idxtype": "unique", "idxfields": ["distributor_id", "pipeline_id"]}, + {"name": "idx_dp_distributor", "idxtype": "index", "idxfields": ["distributor_id"]} + ], + "codes": [ + {"field": "distributor_id", "table": "distributors", "valuefield": "id", "textfield": "name"}, + {"field": "pipeline_id", "table": "pipelines", "valuefield": "id", "textfield": "name"} + ] +} diff --git a/models/distributors.json b/models/distributors.json new file mode 100644 index 0000000..c69dbda --- /dev/null +++ b/models/distributors.json @@ -0,0 +1,24 @@ +{ + "summary": [{"name": "distributors", "title": "分销商表", "primary": ["id"]}], + "fields": [ + {"name": "id", "title": "主键", "type": "str", "length": 32, "nullable": "no"}, + {"name": "name", "title": "分销商名称", "type": "str", "length": 200, "nullable": "no"}, + {"name": "org_id", "title": "关联组织ID", "type": "str", "length": 32}, + {"name": "contact_name", "title": "联系人", "type": "str", "length": 50}, + {"name": "contact_phone", "title": "联系电话", "type": "str", "length": 20}, + {"name": "contact_email", "title": "联系邮箱", "type": "str", "length": 100}, + {"name": "api_key", "title": "API密钥", "type": "str", "length": 200}, + {"name": "commission_rate", "title": "佣金比例%", "type": "double", "length": 5, "dec": 2, "default": "0"}, + {"name": "status", "title": "状态", "type": "str", "length": 20, "default": "active"}, + {"name": "agreement_start", "title": "协议开始日", "type": "date"}, + {"name": "agreement_end", "title": "协议结束日", "type": "date"}, + {"name": "remarks", "title": "备注", "type": "text"}, + {"name": "created_by", "title": "创建人", "type": "str", "length": 32}, + {"name": "created_at", "title": "创建时间", "type": "timestamp"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp"} + ], + "indexes": [ + {"name": "idx_dist_name", "idxtype": "index", "idxfields": ["name"]}, + {"name": "idx_dist_status", "idxtype": "index", "idxfields": ["status"]} + ] +} diff --git a/pipeline_dist/__init__.py b/pipeline_dist/__init__.py new file mode 100644 index 0000000..c584987 --- /dev/null +++ b/pipeline_dist/__init__.py @@ -0,0 +1,13 @@ +from .init import ( + MODULE_NAME, + DBNAME, + distributors_create, + distributors_update, + distributors_delete, + distributor_pipeline_create, + distributor_pipeline_update, + distributor_pipeline_delete, + generate_api_key, + calculate_price, + load_pipeline_dist, +) diff --git a/pipeline_dist/init.py b/pipeline_dist/init.py new file mode 100644 index 0000000..1144c50 --- /dev/null +++ b/pipeline_dist/init.py @@ -0,0 +1,229 @@ +"""pipeline_dist 模块 - 分销商管理模块""" +import json +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools +from ahserver.serverenv import ServerEnv +from appPublic.log import debug + + +MODULE_NAME = 'pipeline_dist' +DBNAME = 'pipeline' + + +def _get_sor(): + """Get database pool and dbname for pipeline_dist module.""" + return DBPools(), DBNAME + + +async def distributors_create(params_kw): + """创建分销商记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + data['id'] = getID() + await sor.C('distributors', data) + result['success'] = True + result['message'] = '创建成功' + result['id'] = data['id'] + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def distributors_update(params_kw): + """更新分销商记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + if not data.get('id'): + result['message'] = '缺少id' + else: + await sor.U('distributors', data) + result['success'] = True + result['message'] = '更新成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def distributors_delete(params_kw): + """删除分销商记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + record_id = params_kw.get('id') + if not record_id: + result['message'] = '缺少id' + else: + await sor.D('distributors', {'id': record_id}) + result['success'] = True + result['message'] = '删除成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def distributor_pipeline_create(params_kw): + """创建分销商-产线关联记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + data['id'] = getID() + await sor.C('distributor_pipeline', data) + result['success'] = True + result['message'] = '创建成功' + result['id'] = data['id'] + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def distributor_pipeline_update(params_kw): + """更新分销商-产线关联记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + data = params_kw.copy() + data.pop('page', None) + data.pop('rows', None) + data.pop('data_filter', None) + if not data.get('id'): + result['message'] = '缺少id' + else: + await sor.U('distributor_pipeline', data) + result['success'] = True + result['message'] = '更新成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def distributor_pipeline_delete(params_kw): + """删除分销商-产线关联记录""" + result = {'success': False, 'message': ''} + try: + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + record_id = params_kw.get('id') + if not record_id: + result['message'] = '缺少id' + else: + await sor.D('distributor_pipeline', {'id': record_id}) + result['success'] = True + result['message'] = '删除成功' + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def generate_api_key(params_kw): + """Generate a new API key for a distributor""" + result = {'success': False, 'message': ''} + try: + distributor_id = params_kw.get('id') or params_kw.get('distributor_id') + if not distributor_id: + result['message'] = '缺少distributor_id' + return json.dumps(result, ensure_ascii=False, default=str) + + new_key = getID() + getID() + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + await sor.U('distributors', {'id': distributor_id, 'api_key': new_key}) + result['success'] = True + result['key'] = new_key + result['distributor_id'] = distributor_id + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +async def calculate_price(params_kw): + """Calculate effective price for a distributor-pipeline combination. + + Returns the effective price considering custom pricing and markup. + If no custom pricing exists, returns None (caller should use base price). + """ + result = {'success': False, 'message': '', 'price': None} + try: + distributor_id = params_kw.get('distributor_id') + pipeline_id = params_kw.get('pipeline_id') + if not distributor_id or not pipeline_id: + result['message'] = '缺少distributor_id或pipeline_id' + return json.dumps(result, ensure_ascii=False, default=str) + + db, dbname = _get_sor() + async with db.sqlorContext(dbname) as sor: + rows = await sor.sqlExe( + 'select * from distributor_pipeline where distributor_id = :distributor_id and pipeline_id = :pipeline_id', + {'distributor_id': distributor_id, 'pipeline_id': pipeline_id} + ) + if not rows: + result['success'] = True + result['price'] = None + return json.dumps(result, ensure_ascii=False, default=str) + + dp = rows[0] if isinstance(rows, list) else rows + + # If custom_price is set, use it as base + base_price = None + custom_price = dp.get('custom_price') if hasattr(dp, 'get') else getattr(dp, 'custom_price', None) + if custom_price is not None: + base_price = custom_price + + # Apply markup if configured + markup_type = dp.get('markup_type') if hasattr(dp, 'get') else getattr(dp, 'markup_type', None) + markup_value = dp.get('markup_value') if hasattr(dp, 'get') else getattr(dp, 'markup_value', None) + + if markup_type and markup_value is not None and base_price is not None: + if markup_type == 'fixed': + result['price'] = base_price + markup_value + elif markup_type == 'percentage': + result['price'] = base_price * (1 + markup_value / 100) + else: + result['price'] = base_price + + result['success'] = True + except Exception as e: + result['message'] = str(e) + return json.dumps(result, ensure_ascii=False, default=str) + + +def load_pipeline_dist(): + """注册函数到 ServerEnv""" + env = ServerEnv() + # Distributors + env.distributors_create = distributors_create + env.distributors_creates = distributors_create + env.distributors_update = distributors_update + env.distributors_updates = distributors_update + env.distributors_delete = distributors_delete + env.distributors_deletes = distributors_delete + # Distributor Pipeline + env.distributor_pipeline_create = distributor_pipeline_create + env.distributor_pipeline_creates = distributor_pipeline_create + env.distributor_pipeline_update = distributor_pipeline_update + env.distributor_pipeline_updates = distributor_pipeline_update + env.distributor_pipeline_delete = distributor_pipeline_delete + env.distributor_pipeline_deletes = distributor_pipeline_delete + # Utility functions + env.generate_api_key = generate_api_key + env.calculate_price = calculate_price + debug(f'[{MODULE_NAME}] module loaded') + return True diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3036833 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "pipeline_dist" +version = "0.1.0" +description = "Pipeline distributor management module" +requires-python = ">=3.8" +dependencies = [ + "sqlor", + "bricks_for_python", +] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..ca77c30 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,22 @@ +"""Register RBAC paths for pipeline_dist module.""" + +RBAC_PATHS = [ + '/api/distributors_list.dspy', + '/api/distributors_get.dspy', + '/api/distributors_save.dspy', + '/api/distributors_delete.dspy', + '/api/distributor_pipeline_list.dspy', + '/api/distributor_pipeline_get.dspy', + '/api/distributor_pipeline_save.dspy', + '/api/distributor_pipeline_delete.dspy', + '/api/distributor_generate_key.dspy', +] + + +def load_paths(register): + """Register all RBAC paths for this module.""" + for path in RBAC_PATHS: + register(f'pipeline_dist{path}', { + 'module': 'pipeline_dist', + 'path': path, + }) diff --git a/wwwroot/api/distributor_generate_key.dspy b/wwwroot/api/distributor_generate_key.dspy new file mode 100644 index 0000000..2bc029f --- /dev/null +++ b/wwwroot/api/distributor_generate_key.dspy @@ -0,0 +1,2 @@ +result = await generate_api_key(params_kw) +return result diff --git a/wwwroot/api/distributor_pipeline_create.dspy b/wwwroot/api/distributor_pipeline_create.dspy new file mode 100644 index 0000000..180ea4b --- /dev/null +++ b/wwwroot/api/distributor_pipeline_create.dspy @@ -0,0 +1,2 @@ +result = await distributor_pipeline_create(params_kw) +return result diff --git a/wwwroot/api/distributor_pipeline_delete.dspy b/wwwroot/api/distributor_pipeline_delete.dspy new file mode 100644 index 0000000..9d54cc0 --- /dev/null +++ b/wwwroot/api/distributor_pipeline_delete.dspy @@ -0,0 +1,2 @@ +result = await distributor_pipeline_delete(params_kw) +return result diff --git a/wwwroot/api/distributor_pipeline_update.dspy b/wwwroot/api/distributor_pipeline_update.dspy new file mode 100644 index 0000000..16ee224 --- /dev/null +++ b/wwwroot/api/distributor_pipeline_update.dspy @@ -0,0 +1,2 @@ +result = await distributor_pipeline_update(params_kw) +return result diff --git a/wwwroot/api/distributors_create.dspy b/wwwroot/api/distributors_create.dspy new file mode 100644 index 0000000..999d8b5 --- /dev/null +++ b/wwwroot/api/distributors_create.dspy @@ -0,0 +1,2 @@ +result = await distributors_create(params_kw) +return result diff --git a/wwwroot/api/distributors_delete.dspy b/wwwroot/api/distributors_delete.dspy new file mode 100644 index 0000000..a666ef2 --- /dev/null +++ b/wwwroot/api/distributors_delete.dspy @@ -0,0 +1,2 @@ +result = await distributors_delete(params_kw) +return result diff --git a/wwwroot/api/distributors_update.dspy b/wwwroot/api/distributors_update.dspy new file mode 100644 index 0000000..7be53b3 --- /dev/null +++ b/wwwroot/api/distributors_update.dspy @@ -0,0 +1,2 @@ +result = await distributors_update(params_kw) +return result diff --git a/wwwroot/api/get_search_distributor_id.dspy b/wwwroot/api/get_search_distributor_id.dspy new file mode 100644 index 0000000..238b917 --- /dev/null +++ b/wwwroot/api/get_search_distributor_id.dspy @@ -0,0 +1,8 @@ +data = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'pipeline') as sor: + rows = await sor.sqlExe('select id as value, name as text from distributors order by name', {}) + data.extend(list(rows)) +except Exception as e: + debug('error: ' + str(e)) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/api/get_search_pipeline_id.dspy b/wwwroot/api/get_search_pipeline_id.dspy new file mode 100644 index 0000000..5519023 --- /dev/null +++ b/wwwroot/api/get_search_pipeline_id.dspy @@ -0,0 +1,8 @@ +data = [{'value': '', 'text': '全部'}] +try: + async with get_sor_context(request._run_ns, 'pipeline') as sor: + rows = await sor.sqlExe('select id as value, name as text from pipelines order by name', {}) + data.extend(list(rows)) +except Exception as e: + debug('error: ' + str(e)) +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..d234498 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,16 @@ +{ + "title": "分销管理", + "layout": "cards", + "cards": [ + { + "title": "分销商管理", + "icon": "people", + "url": "{{entire_url('../json/distributors.json')}}" + }, + { + "title": "分销商产线配置", + "icon": "link", + "url": "{{entire_url('../json/distributor_pipeline.json')}}" + } + ] +}