diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..359beb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +.venv/ +py3/ +build/ +dist/ +*.egg-info/ +*.egg +wwwroot/wwwroot +mysql.ddl.sql diff --git a/app/init_permissions.py b/app/init_permissions.py new file mode 100644 index 0000000..fdee831 --- /dev/null +++ b/app/init_permissions.py @@ -0,0 +1,152 @@ +"""Permission initialization for integrated CRM application. + +Reads perm_config.py and registers permissions to database. +This should be run during deployment or first-time setup. + +Usage: + # During deployment + python app/init_permissions.py + + # Or called from integrated_crm_app.py init() +""" +import os +import sys +import asyncio +from appPublic.uniqueID import getID +from appPublic.log import debug, info +from sqlor.dbpools import DBPools + + +async def ensure_permission(sor, path, name='', permtype='page'): + """Ensure a permission exists in the database.""" + recs = await sor.R('permission', {'path': path}) + if recs: + return recs[0].id + permid = getID() + await sor.C('permission', { + 'id': permid, + 'name': name or path.split('/')[-1] or path, + 'path': path, + 'permtype': permtype, + }) + return permid + + +async def ensure_role(sor, roleid, name, desc=''): + """Ensure a role exists with the given ID.""" + recs = await sor.R('role', {'id': roleid}) + if recs: + return recs[0].id + await sor.C('role', { + 'id': roleid, + 'orgtypeid': '*', + 'name': name, + }) + return roleid + + +async def grant_permission(sor, roleid, permid): + """Grant a permission to a role if not already granted.""" + recs = await sor.R('rolepermission', {'roleid': roleid, 'permid': permid}) + if not recs: + await sor.C('rolepermission', { + 'id': getID(), + 'roleid': roleid, + 'permid': permid, + }) + + +async def init_permissions_from_config(dbname, config_module=None): + """Initialize all permissions from perm_config.py. + + CRM是单业主机构系统,所有角色属于同一业主机构。 + 流程: + 1. 创建约定角色(any/logined/anonymous)和定义的角色 + 2. 注册 PERMISSION_MATRIX 中的路径权限 + 3. 注册 CRUD 路径并授权 + """ + if config_module is None: + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + from app.perm_config import ( + ROLES, PERMISSION_MATRIX, CRUD_TABLES + ) + else: + ROLES = config_module.ROLES + PERMISSION_MATRIX = config_module.PERMISSION_MATRIX + CRUD_TABLES = config_module.CRUD_TABLES + + db = DBPools() + async with db.sqlorContext(dbname) as sor: + # Step 1: 创建约定角色(固定ID,rbac/userperm.py硬编码检查) + info('Creating convention roles...') + role_ids = {} + + # 约定角色必须用固定ID + for fixed_id in ['any', 'logined', 'anonymous']: + role_ids[fixed_id] = await ensure_role(sor, fixed_id, fixed_id) + + # Step 2: 创建定义的角色 + info('Creating defined roles...') + for role in ROLES: + role_ids[role['id']] = await ensure_role( + sor, role['id'], role['name'], role.get('desc', '') + ) + + # Step 3: 注册权限矩阵中的路径并授权 + info('Registering permissions from matrix...') + perm_count = 0 + + for module, paths in PERMISSION_MATRIX.items(): + for path_pattern, role_list in paths.items(): + permid = await ensure_permission(sor, path_pattern, + name=f'{module}: {path_pattern}', permtype='module') + + for role_name in role_list: + if role_name in role_ids: + await grant_permission(sor, role_ids[role_name], permid) + perm_count += 1 + + # Step 4: 注册 CRUD 路径 + info('Registering CRUD paths...') + for module, tables in CRUD_TABLES.items(): + for table in tables: + crud_path = f'/{module}/{table}/' + permid = await ensure_permission(sor, crud_path, + name=f'{module}/{table} CRUD', permtype='crud') + + # 根据模块权限矩阵授予权限 + if module in PERMISSION_MATRIX: + for path_pattern, role_list in PERMISSION_MATRIX[module].items(): + if path_pattern.startswith(f'/{module}'): + for role_name in role_list: + if role_name in role_ids: + await grant_permission(sor, role_ids[role_name], permid) + perm_count += 1 + + info(f'Permission initialization complete: {perm_count} grants created') + + +def main(): + """Run permission initialization.""" + import json + from appPublic.jsonConfig import getConfig + from appPublic.dictObject import DictObject + + config_path = os.path.dirname(os.path.dirname(__file__)) + config = getConfig(config_path, {'workdir': config_path}) + + # Convert database config to DictObject format expected by DBPools + db_config = {} + for dbname, dbconf in config.databases.items(): + db_config[dbname] = DictObject(driver=dbconf['driver'], kwargs=DictObject(**dbconf['kwargs'])) + + DBPools(db_config) + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + dbname = list(config.databases.keys())[0] + loop.run_until_complete(init_permissions_from_config(dbname)) + + +if __name__ == '__main__': + main() diff --git a/app/integrated_crm_app.py b/app/integrated_crm_app.py index 2da1b5c..9c3c440 100644 --- a/app/integrated_crm_app.py +++ b/app/integrated_crm_app.py @@ -1,5 +1,6 @@ from ahserver.webapp import webapp from ahserver.serverenv import ServerEnv +import os # Import required modules using the standard pattern # Foundation modules @@ -31,6 +32,17 @@ def init(): # Load modules in dependency order load_appbase() load_rbac() + + # Initialize permissions from perm_config.py + # This ensures all UI/DSPY paths and CRUD paths are registered + import sys + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + from app.init_permissions import init_permissions_from_config + import asyncio + loop = asyncio.new_event_loop() + loop.run_until_complete(init_permissions_from_config('crm_db')) + loop.close() + load_customer_management() load_opportunity_management() load_contract_management() diff --git a/app/perm_config.py b/app/perm_config.py new file mode 100644 index 0000000..27e52d2 --- /dev/null +++ b/app/perm_config.py @@ -0,0 +1,156 @@ +"""CRM Permission Configuration + +单一业主机构内的角色和权限配置。 +部署时由管理员通过RBAC UI或此配置初始化。 + +Convention roles: + - any: 任何人(未登录) + - logined: 已登录用户 + - anonymous: 匿名用户 +""" + +# ============================================================ +# 单一业主机构 +# ============================================================ +# CRM为单业主机构系统,不跨机构。 +# 所有角色属于同一业主机构内部的不同部门。 + +# ============================================================ +# 角色定义 +# ============================================================ +ROLES = [ + # 销售部 + {'id': 'sales_manager', 'name': '销售经理', 'desc': '团队管理、客户分配、审批'}, + {'id': 'sales_rep', 'name': '销售代表', 'desc': '客户跟进、商机推进、合同起草'}, + # 财务部 + {'id': 'finance_admin', 'name': '财务管理员', 'desc': '所有财务操作、报表'}, + {'id': 'finance_clerk', 'name': '财务出纳', 'desc': '收款登记、付款处理'}, + # 管理员 + {'id': 'admin_superuser', 'name': '超级用户', 'desc': '全部权限,初始化用'}, +] + +# ============================================================ +# 权限矩阵 +# 格式: {模块名: {路径模式: [有权限的角色列表]}} +# ============================================================ +PERMISSION_MATRIX = { + # ---------------------------------------------------------- + # 公共路径(登录等) + # ---------------------------------------------------------- + 'main': { + '/main/login.ui': ['any'], + '/main/login.dspy': ['any'], + }, + + # ---------------------------------------------------------- + # 客户管理模块 - 销售+财务+管理员 + # ---------------------------------------------------------- + 'customer_management': { + '/customer_management/**': [ + 'sales_manager', 'sales_rep', + 'finance_admin', + 'admin_superuser', + ], + '/customer_management/customer_handover**': [ + 'sales_manager', 'sales_rep', + 'admin_superuser', + ], + '/customer_management/customer_pool**': [ + 'sales_manager', 'sales_rep', + 'admin_superuser', + ], + }, + + # ---------------------------------------------------------- + # 商机管理模块 - 销售+管理员 + # ---------------------------------------------------------- + 'opportunity_management': { + '/opportunity_management/**': [ + 'sales_manager', 'sales_rep', + 'admin_superuser', + ], + }, + + # ---------------------------------------------------------- + # 合同管理模块 - 销售+财务+管理员 + # ---------------------------------------------------------- + 'contract_management': { + '/contract_management/**': [ + 'sales_manager', 'sales_rep', + 'finance_admin', + 'admin_superuser', + ], + '/contract_management/contract_ai_config**': [ + 'sales_manager', + 'admin_superuser', + ], + }, + + # ---------------------------------------------------------- + # 财务管理模块 - 财务+管理员 + # ---------------------------------------------------------- + 'financial_management': { + '/financial_management/**': [ + 'finance_admin', 'finance_clerk', + 'admin_superuser', + ], + '/financial_management/receivables**': [ + 'sales_manager', + 'finance_admin', 'finance_clerk', + 'admin_superuser', + ], + }, + + # ---------------------------------------------------------- + # 工作流审批模块 - 全部内部角色 + # ---------------------------------------------------------- + 'workflow_approval': { + '/workflow_approval/**': [ + 'sales_manager', 'sales_rep', + 'finance_admin', 'finance_clerk', + 'admin_superuser', + ], + }, + + # ---------------------------------------------------------- + # 统一仪表盘 - 全部内部角色 + # ---------------------------------------------------------- + 'unified_dashboard': { + '/unified_dashboard/**': [ + 'sales_manager', 'sales_rep', + 'finance_admin', 'finance_clerk', + 'admin_superuser', + ], + }, +} + +# ============================================================ +# CRUD 路径 +# 每个模块的表自动生成 CRUD 路径: /{modulename}/{tablename}/ +# ============================================================ +CRUD_TABLES = { + 'customer_management': [ + 'customers', 'customer_pool', + 'customer_handover', 'customer_handover_items', + ], + 'opportunity_management': [ + 'opportunities', 'opportunity_stage_history', + 'opportunity_predictions', 'sales_stages', + ], + 'contract_management': [ + 'contract', 'contract_versions', 'contract_attachment', + 'contract_milestones', 'contract_ai_config', + 'orders', 'order_payments', + ], + 'financial_management': [ + 'receivables', 'receipts', 'receipt_allocations', + 'payments', 'financial_vouchers', + ], + 'workflow_approval': [ + 'approval_workflow', 'approval_instance', + 'approval_step', 'approval_task', + ], + 'unified_dashboard': [ + 'dashboard_config', 'report_template', 'user_dashboard', + ], +}