From a77df5da0fe1520933388dbc2e1c4b947fa3e657 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 25 Jun 2026 17:56:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Main.py | 2 +- TempLate/GERP/_base | 2 +- TempLate/GERP/assets/css/Component.css | 23 + TempLate/GERP/assets/css/GERP.css | 17 + TempLate/GERP/assets/gerp/CompanyCommon.js | 468 +++++ TempLate/GERP/assets/gerp/GERPUI/GERPUI.js | 106 +- TempLate/GERP/assets/gerp/TableEngine.js | 90 +- TempLate/GERP/assets/gerp/gerp.js | 16 +- .../GERP/pages/Company/company-detail.html | 244 +++ TempLate/GERP/pages/Company/company.html | 38 + .../GERP/pages/Company/department-detail.html | 152 ++ TempLate/GERP/pages/Company/department.html | 39 + .../GERP/pages/Company/employee-detail.html | 187 ++ .../GERP/pages/Company/workgroup-detail.html | 145 ++ TempLate/GERP/pages/Company/workgroup.html | 35 + TempLate/GERP/pages/system/role-detail.html | 261 +++ TempLate/GERP/pages/system/roles.html | 118 ++ TempLate/GERP/pages/system/user-detail.html | 227 +++ TempLate/GERP/pages/system/users.html | 212 +-- TempLate/login.html | 181 +- TempLate/register.html | 145 +- gvsds/__pycache__/model_base.cpython-312.pyc | Bin 524 -> 455 bytes gvsds/__pycache__/urls.cpython-312.pyc | Bin 21435 -> 21803 bytes .../__pycache__/manager.cpython-312.pyc | Bin 19606 -> 19606 bytes .../core/__pycache__/models.cpython-312.pyc | Bin 20283 -> 20549 bytes .../core/__pycache__/views.cpython-312.pyc | Bin 7270 -> 7215 bytes gvsds/apps/core/models.py | 13 +- gvsds/apps/core/views.py | 7 +- .../__pycache__/form_service.cpython-312.pyc | Bin 21290 -> 36342 bytes .../gerp/__pycache__/models.cpython-312.pyc | Bin 54175 -> 56868 bytes gvsds/apps/gerp/form_service.py | 372 +++- .../migrations/0004_remove_tenant_code.py | 2 +- .../0005_rolepermission_companyuuid.py | 36 + .../0006_userrole_dept_workgroup.py | 22 + .../gerp/migrations/0007_userrole_roletype.py | 16 + .../migrations/0008_workgroup_companyuuid.py | 17 + .../0004_remove_tenant_code.cpython-312.pyc | Bin 0 -> 614 bytes ...rolepermission_companyuuid.cpython-312.pyc | Bin 0 -> 2295 bytes ...06_userrole_dept_workgroup.cpython-312.pyc | Bin 0 -> 1355 bytes .../0007_userrole_roletype.cpython-312.pyc | Bin 0 -> 785 bytes ...0008_workgroup_companyuuid.cpython-312.pyc | Bin 0 -> 1007 bytes gvsds/apps/gerp/models.py | 90 +- gvsds/apps/gerp/views/__init__.py | 3 +- .../__pycache__/__init__.cpython-312.pyc | Bin 3812 -> 3867 bytes .../__pycache__/form_api.cpython-312.pyc | Bin 10422 -> 12676 bytes .../__pycache__/user_detail.cpython-312.pyc | Bin 17303 -> 17566 bytes .../__pycache__/viewsets.cpython-312.pyc | Bin 44251 -> 46125 bytes gvsds/apps/gerp/views/form_api.py | 88 +- gvsds/apps/gerp/views/user_detail.py | 10 +- gvsds/apps/gerp/views/viewsets.py | 50 +- .../__pycache__/serializers.cpython-312.pyc | Bin 3132 -> 3136 bytes .../ugm/__pycache__/views.cpython-312.pyc | Bin 22803 -> 25204 bytes gvsds/apps/ugm/serializers.py | 2 +- gvsds/apps/ugm/views.py | 61 +- gvsds/model_base.py | 3 +- gvsds/urls.py | 2 + gvsdsdk/__pycache__/__init__.cpython-312.pyc | Bin 12530 -> 12521 bytes gvsdsdk/__pycache__/__main__.cpython-312.pyc | Bin 0 -> 3590 bytes gvsdsdk/__pycache__/apps.cpython-312.pyc | Bin 630 -> 621 bytes .../business_models.cpython-312.pyc | Bin 0 -> 7791 bytes gvsdsdk/__pycache__/db_router.cpython-312.pyc | Bin 0 -> 3964 bytes gvsdsdk/__pycache__/fluent.cpython-312.pyc | Bin 34856 -> 46353 bytes .../__pycache__/form_service.cpython-312.pyc | Bin 0 -> 19744 bytes .../__pycache__/model_base.cpython-312.pyc | Bin 20880 -> 29044 bytes .../__pycache__/model_utils.cpython-312.pyc | Bin 3133 -> 3124 bytes gvsdsdk/__pycache__/models.cpython-312.pyc | Bin 43287 -> 44050 bytes .../auth/__pycache__/__init__.cpython-312.pyc | Bin 1943 -> 1934 bytes .../auth/__pycache__/cookie.cpython-312.pyc | Bin 1535 -> 1526 bytes .../auth/__pycache__/crypto.cpython-312.pyc | Bin 4542 -> 4533 bytes .../auth/__pycache__/elt_auth.cpython-312.pyc | Bin 2376 -> 2402 bytes .../auth/__pycache__/jwt_auth.cpython-312.pyc | Bin 0 -> 2710 bytes .../auth/__pycache__/managers.cpython-312.pyc | Bin 0 -> 14947 bytes .../__pycache__/middleware.cpython-312.pyc | Bin 2861 -> 5791 bytes .../__pycache__/permissions.cpython-312.pyc | Bin 5236 -> 5511 bytes gvsdsdk/auth/elt_auth.py | 3 +- gvsdsdk/auth/jwt_auth.py | 54 + gvsdsdk/auth/managers.py | 23 +- gvsdsdk/auth/middleware.py | 85 +- gvsdsdk/auth/permissions.py | 22 +- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1232 bytes .../__pycache__/manager.cpython-312.pyc | Bin 0 -> 19588 bytes .../__pycache__/models.cpython-312.pyc | Bin 0 -> 18090 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 766 bytes .../__pycache__/engine.cpython-312.pyc | Bin 0 -> 8382 bytes gvsdsdk/fluent.py | 742 +++++--- .../__pycache__/__init__.cpython-312.pyc | Bin 138 -> 129 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 138 bytes .../__pycache__/gvsdsdk_init.cpython-312.pyc | Bin 0 -> 6976 bytes .../hash_passwords.cpython-312.pyc | Bin 0 -> 1667 bytes gvsdsdk/management/commands/hash_passwords.py | 30 + gvsdsdk/migrations/0001_initial.py | 254 ++- .../migrations/0002_alter_user_useruuid.py | 20 + .../__pycache__/0001_initial.cpython-312.pyc | Bin 9719 -> 15882 bytes .../0002_alter_user_useruuid.cpython-312.pyc | Bin 0 -> 930 bytes gvsdsdk/model_base.py | 473 +++-- gvsdsdk/models.py | 1639 +++++++++-------- .../__pycache__/__init__.cpython-312.pyc | Bin 1853 -> 1844 bytes .../msync/__pycache__/auth.cpython-312.pyc | Bin 6395 -> 6386 bytes .../msync/__pycache__/client.cpython-312.pyc | Bin 14180 -> 14171 bytes .../__pycache__/local_tx.cpython-312.pyc | Bin 5486 -> 5477 bytes gvsdsdk/msync/__pycache__/ogm.cpython-312.pyc | Bin 0 -> 366 bytes .../__pycache__/orm_handler.cpython-312.pyc | Bin 25215 -> 25206 bytes .../__pycache__/registry.cpython-312.pyc | Bin 0 -> 377 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 858 -> 849 bytes .../order/__pycache__/models.cpython-312.pyc | Bin 22425 -> 22416 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 1867 -> 1858 bytes .../__pycache__/manager.cpython-312.pyc | Bin 0 -> 17858 bytes .../__pycache__/models.cpython-312.pyc | Bin 0 -> 11047 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 832 -> 823 bytes .../__pycache__/alipay.cpython-312.pyc | Bin 11891 -> 11882 bytes .../gateways/__pycache__/bank.cpython-312.pyc | Bin 5574 -> 5565 bytes .../gateways/__pycache__/base.cpython-312.pyc | Bin 10440 -> 10431 bytes .../__pycache__/registry.cpython-312.pyc | Bin 4025 -> 4016 bytes .../__pycache__/wechat.cpython-312.pyc | Bin 8866 -> 8857 bytes 114 files changed, 5183 insertions(+), 1664 deletions(-) create mode 100644 TempLate/GERP/assets/gerp/CompanyCommon.js create mode 100644 TempLate/GERP/pages/Company/company-detail.html create mode 100644 TempLate/GERP/pages/Company/company.html create mode 100644 TempLate/GERP/pages/Company/department-detail.html create mode 100644 TempLate/GERP/pages/Company/department.html create mode 100644 TempLate/GERP/pages/Company/employee-detail.html create mode 100644 TempLate/GERP/pages/Company/workgroup-detail.html create mode 100644 TempLate/GERP/pages/Company/workgroup.html create mode 100644 TempLate/GERP/pages/system/role-detail.html create mode 100644 TempLate/GERP/pages/system/roles.html create mode 100644 TempLate/GERP/pages/system/user-detail.html create mode 100644 gvsds/apps/gerp/migrations/0005_rolepermission_companyuuid.py create mode 100644 gvsds/apps/gerp/migrations/0006_userrole_dept_workgroup.py create mode 100644 gvsds/apps/gerp/migrations/0007_userrole_roletype.py create mode 100644 gvsds/apps/gerp/migrations/0008_workgroup_companyuuid.py create mode 100644 gvsds/apps/gerp/migrations/__pycache__/0004_remove_tenant_code.cpython-312.pyc create mode 100644 gvsds/apps/gerp/migrations/__pycache__/0005_rolepermission_companyuuid.cpython-312.pyc create mode 100644 gvsds/apps/gerp/migrations/__pycache__/0006_userrole_dept_workgroup.cpython-312.pyc create mode 100644 gvsds/apps/gerp/migrations/__pycache__/0007_userrole_roletype.cpython-312.pyc create mode 100644 gvsds/apps/gerp/migrations/__pycache__/0008_workgroup_companyuuid.cpython-312.pyc create mode 100644 gvsdsdk/__pycache__/__main__.cpython-312.pyc create mode 100644 gvsdsdk/__pycache__/business_models.cpython-312.pyc create mode 100644 gvsdsdk/__pycache__/db_router.cpython-312.pyc create mode 100644 gvsdsdk/__pycache__/form_service.cpython-312.pyc create mode 100644 gvsdsdk/auth/__pycache__/jwt_auth.cpython-312.pyc create mode 100644 gvsdsdk/auth/__pycache__/managers.cpython-312.pyc create mode 100644 gvsdsdk/auth/jwt_auth.py create mode 100644 gvsdsdk/commission/__pycache__/__init__.cpython-312.pyc create mode 100644 gvsdsdk/commission/__pycache__/manager.cpython-312.pyc create mode 100644 gvsdsdk/commission/__pycache__/models.cpython-312.pyc create mode 100644 gvsdsdk/commission/strategies/__pycache__/__init__.cpython-312.pyc create mode 100644 gvsdsdk/commission/strategies/__pycache__/engine.cpython-312.pyc create mode 100644 gvsdsdk/management/commands/__pycache__/__init__.cpython-312.pyc create mode 100644 gvsdsdk/management/commands/__pycache__/gvsdsdk_init.cpython-312.pyc create mode 100644 gvsdsdk/management/commands/__pycache__/hash_passwords.cpython-312.pyc create mode 100644 gvsdsdk/management/commands/hash_passwords.py create mode 100644 gvsdsdk/migrations/0002_alter_user_useruuid.py create mode 100644 gvsdsdk/migrations/__pycache__/0002_alter_user_useruuid.cpython-312.pyc create mode 100644 gvsdsdk/msync/__pycache__/ogm.cpython-312.pyc create mode 100644 gvsdsdk/msync/__pycache__/registry.cpython-312.pyc create mode 100644 gvsdsdk/payment/__pycache__/manager.cpython-312.pyc create mode 100644 gvsdsdk/payment/__pycache__/models.cpython-312.pyc diff --git a/Main.py b/Main.py index 6761d9da..7c3bf3a4 100644 --- a/Main.py +++ b/Main.py @@ -214,9 +214,9 @@ class FilteredLoggingMiddleware: return self.app(environ, wrapped_start_response) -import re import threading import time +import re class SecurityMiddleware: def __init__(self, app): diff --git a/TempLate/GERP/_base b/TempLate/GERP/_base index 7ceff088..e10ca79a 100644 --- a/TempLate/GERP/_base +++ b/TempLate/GERP/_base @@ -24,7 +24,7 @@ - + diff --git a/TempLate/GERP/assets/css/Component.css b/TempLate/GERP/assets/css/Component.css index 176fd820..993b0b4c 100644 --- a/TempLate/GERP/assets/css/Component.css +++ b/TempLate/GERP/assets/css/Component.css @@ -233,6 +233,20 @@ margin-top: -2px; } +/* 半选状态(横线) */ +.GERPCheckbox input:indeterminate + .GERPCheckboxBox { + background: var(--PrimaryColor); + border-color: var(--PrimaryColor); +} + +.GERPCheckbox input:indeterminate + .GERPCheckboxBox::after { + content: ''; + width: 10px; + height: 0; + border-bottom: 2px solid white; + margin-top: 0; +} + .GERPCheckbox input:disabled + .GERPCheckboxBox { opacity: 0.5; cursor: not-allowed; @@ -458,6 +472,15 @@ align-items: center; justify-content: space-between; padding: 16px 20px 14px; + cursor: move; + user-select: none; +} + +/* 手机端对话框标题不显示拖动光标 */ +@media (max-width: 768px) { + .GERPDialogHeader { + cursor: default; + } } .GERPDialogTitle { diff --git a/TempLate/GERP/assets/css/GERP.css b/TempLate/GERP/assets/css/GERP.css index d1ff40f8..aac3c224 100644 --- a/TempLate/GERP/assets/css/GERP.css +++ b/TempLate/GERP/assets/css/GERP.css @@ -3396,6 +3396,15 @@ body.GERPSidebarOpen .GERPSidebar { padding: 20px 24px; border-bottom: 1px solid var(--BorderColor); flex-shrink: 0; + cursor: move; + user-select: none; +} + +/* 手机端抽屉标题不显示拖动光标 */ +@media (max-width: 768px) { + .GERPDrawerHeader { + cursor: default; + } } .GERPDrawerTitle { @@ -4369,6 +4378,14 @@ body.GERPSidebarOpen .GERPSidebar { justify-content: space-between; padding: 16px 20px; border-bottom: 1px solid var(--BorderColor); + cursor: move; + user-select: none; +} + +@media (max-width: 768px) { + .GERPDrawerHeader { + cursor: default; + } } .GERPDrawerTitle { diff --git a/TempLate/GERP/assets/gerp/CompanyCommon.js b/TempLate/GERP/assets/gerp/CompanyCommon.js new file mode 100644 index 00000000..5e801b04 --- /dev/null +++ b/TempLate/GERP/assets/gerp/CompanyCommon.js @@ -0,0 +1,468 @@ +/** + * CompanyCommon.js — Company/* 页面共享组件 + * 提供工具函数、TablePage 基类和共享样式,消除重复代码。 + * 所有 Company/* 页面通过 引入。 + */ + +// ==================== 共享样式(仅注入一次) ==================== +(function () { + if (document.getElementById('CompanyCommonStyles')) return; + const style = document.createElement('style'); + style.id = 'CompanyCommonStyles'; + style.textContent = ` + .GERPPage { padding: 24px; max-width: 1400px; margin: 0 auto; height: 100%; display: flex; flex-direction: column; box-sizing: border-box; } + .cp-page { padding: 20px; flex: 1; min-height: 0; display: flex; flex-direction: column; box-sizing: border-box; } + .cp-header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; flex-shrink: 0; } + .cp-header h1 { margin: 0; flex: 1; } + .cp-container { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; } + + /* 详情面板 */ + .cp-detail-section { margin-bottom: 20px; } + .cp-detail-section h3 { font-size: 14px; font-weight: 600; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 1px solid var(--BorderColor); display: flex; align-items: center; gap: 8px; } + .cp-detail-section h3 i { color: var(--PrimaryColor); } + .cp-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; } + .cp-info-item { font-size: 13px; } + .cp-info-item .label { color: var(--TextSecondary); font-size: 12px; } + .cp-info-item .value { color: var(--TextPrimary); font-weight: 500; word-break: break-all; } + .cp-sticky-info { position: sticky; top: 0; z-index: 10; background: var(--BgColor, #fff); padding: 12px 0; border-bottom: 1px solid var(--BorderColor); margin-bottom: 16px; } + .cp-detail-list { list-style: none; padding: 0; margin: 0; } + .cp-detail-list li { padding: 8px 12px; border: 1px solid var(--BorderColor); border-radius: 6px; margin-bottom: 6px; font-size: 13px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + .cp-detail-list li .item-name { font-weight: 500; flex: 1; } + .cp-detail-list li .item-desc { color: var(--TextSecondary); font-size: 12px; } + .cp-empty { text-align: center; padding: 20px; color: var(--TextSecondary); font-size: 13px; } + .cp-error { text-align: center; padding: 40px; color: var(--DangerColor); font-size: 14px; } + .cp-loading { text-align: center; padding: 40px; color: var(--TextSecondary); font-size: 13px; } + + /* 返回按钮 */ + .cp-back-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--BorderColor); border-radius: 6px; background: var(--BgColor, #fff); color: var(--TextPrimary); font-size: 13px; cursor: pointer; transition: all .2s; text-decoration: none; } + .cp-back-btn:hover { border-color: var(--PrimaryColor); color: var(--PrimaryColor); } + `; + document.head.appendChild(style); +})(); + +// ==================== 工具函数 ==================== +const CompanyUtils = { + // 实体选择器共享配置 + EntityPickerConfigs: { + User: { + api: '/API/FormAPI/UserSearch', + labelField: 'UserName', + valueField: 'UserUUID', + secondaryField: 'UserEmail', + placeholder: '输入用户名搜索...', + }, + Role: { + api: '/API/FormAPI/RoleSearch', + labelField: 'RoleName', + valueField: 'RoleUUID', + placeholder: '输入角色名搜索...', + }, + Company: { + api: '/API/FormAPI/CompanySearch', + labelField: 'CompanyName', + valueField: 'CompanyUUID', + placeholder: '输入公司名搜索...', + }, + Department: { + api: '/API/FormAPI/DepartmentSearch', + labelField: 'DeptName', + valueField: 'DeptUUID', + placeholder: '输入部门名搜索...', + }, + Workgroup: { + api: '/API/FormAPI/WorkgroupSearch', + labelField: 'WorkgroupName', + valueField: 'WorkgroupUUID', + placeholder: '输入工作组名搜索...', + }, + Permission: { + api: '/API/FormAPI/PermissionSearch', + labelField: 'PermName', + valueField: 'PermUUID', + secondaryField: 'PermCode', + placeholder: '输入权限名搜索...', + }, + }, + + /** 生成 entityPicker 列配置 */ + epColumn(field, title, entityName, extra = {}) { + const ep = this.EntityPickerConfigs[entityName]; + if (!ep) return { field, title, ...extra }; + return { + field, title, type: 'entityPicker', entityPicker: ep, ...extra, + }; + }, + + /** HTML 转义 */ + esc(s) { + if (s == null) return ''; + const d = document.createElement('div'); + d.textContent = String(s); + return d.innerHTML; + }, + + /** 状态标签 */ + statusTag(status) { + if (status === 1 || status === '1') return '启用'; + return '禁用'; + }, + + /** 时间格式化 */ + formatTime(t) { + if (!t) return '-'; + try { const d = new Date(t + (t.endsWith('Z') ? '' : 'Z')); return d.toLocaleString(); } catch { return t; } + }, + + /** 角色类型标签(0=组员/员工, 1=组长/负责人) */ + roleTypeTag(roleType, context) { + const blue = { bg: '#dbeafe', color: '#1e40af' }; + const purple = { bg: '#ede9fe', color: '#6d28d9' }; + let label = '', style = blue; + if (context === 'dept') { + if (roleType === 0) { label = '员工'; style = blue; } + else if (roleType === 1) { label = '负责人'; style = purple; } + else return ''; + } else if (context === 'workgroup') { + if (roleType === 0) { label = '组员'; style = blue; } + else if (roleType === 1) { label = '组长'; style = purple; } + else return ''; + } else { + if (roleType === 0) { label = '组员'; style = blue; } + else if (roleType === 1) { label = '组长'; style = purple; } + else return ''; + } + return `${label}`; + }, + + /** 等待 GERPUI 就绪 */ + waitForUI(callback) { + if (window.GERPUI && typeof window.GERPUI.OpenDrawer === 'function') { + callback(window.GERPUI); + return; + } + document.addEventListener('GERPUIReady', () => callback(window.GERPUI), { once: true }); + }, + + /** 从 URL 获取 server 参数 */ + getServer() { + return new URLSearchParams(window.location.search).get('server') || 'gvsds-master'; + }, + + /** 设置页面标题 */ + setPageTitle(title) { + const server = this.getServer(); + const fullTitle = server === 'gvsds-master' ? title : `${server} ${title}`; + if (typeof GERP !== 'undefined' && GERP.SetTitle) GERP.SetTitle(fullTitle); + const el = document.getElementById('page-title'); + if (el) el.textContent = server === 'gvsds-master' ? title : `${server} - ${title}`; + }, + + /** 跳转到详情页 */ + navigateTo(pagePath, pkName, pkValue, title) { + const server = this.getServer(); + const params = new URLSearchParams({ [pkName]: pkValue }); + if (server !== 'gvsds-master') params.set('server', server); + const url = `${pagePath}?${params}`; + console.log('[CompanyUtils.navigateTo]', { pagePath, pkName, pkValue, title, url }); + if (window.GERP && typeof window.GERP.Navigate === 'function') { + window.GERP.Navigate(url, title || ''); + } else { + console.warn('[CompanyUtils.navigateTo] GERP.Navigate 不可用,使用 location.href 跳转'); + window.location.href = url; + } + }, + + // ---- UUID 解析缓存 ---- + _uuidCache: {}, + + /** + * 批量解析 UUID 到名称 + * @param {Object} uuidMap - 格式: { key: {table:'Company', uuids:['xxx','yyy']}, ... } + * @returns {Object} - 格式: { key: {'xxx':'公司A', 'yyy':'公司B'}, ... } + */ + async resolveUUIDs(uuidMap) { + const server = this.getServer(); + const nameFieldMap = { + Company: 'CompanyName', + Department: 'DeptName', + Workgroup: 'WorkgroupName', + UserRole: 'RoleName', + }; + const pkFieldMap = { + Company: 'CompanyUUID', + Department: 'DeptUUID', + Workgroup: 'WorkgroupUUID', + UserRole: 'UserRoleUUID', + }; + + const result = {}; + const fetchPromises = []; + + for (const [key, info] of Object.entries(uuidMap)) { + result[key] = {}; + const { table, uuids } = info; + const nameField = nameFieldMap[table] || 'Name'; + const pkField = pkFieldMap[table] || (table + 'UUID'); + + for (const uuid of uuids) { + if (!uuid) continue; + // 检查缓存 + const cacheKey = `${table}:${uuid}`; + if (this._uuidCache[cacheKey] !== undefined) { + result[key][uuid] = this._uuidCache[cacheKey]; + continue; + } + // 需要请求 + fetchPromises.push( + this._fetchUUIDName(table, pkField, nameField, uuid, server).then(name => { + this._uuidCache[cacheKey] = name; + result[key][uuid] = name; + }).catch(() => { + result[key][uuid] = uuid; + }) + ); + } + } + + await Promise.all(fetchPromises); + return result; + }, + + /** 内部:获取单个 UUID 对应的名称 */ + async _fetchUUIDName(table, pkField, nameField, uuid, server) { + const params = { PageSize: '1' }; + params[pkField] = uuid; + if (server && server !== 'gvsds-master') params.server = server; + const qs = new URLSearchParams(params).toString(); + const url = `/API/FormAPI/Get/${table}?${qs}`; + const resp = await fetch(url, { credentials: 'include' }); + const json = await resp.json(); + if (json.code === 0 && json.data && json.data.length > 0) { + return json.data[0][nameField] || uuid; + } + return uuid; + }, + + /** + * 通用编辑对话框 + * @param {Object} config + * @param {string} config.tableName - 表名 + * @param {Object} config.data - 当前数据(含主键) + * @param {Array} [config.columns] - 列配置(可选,不传则自动获取) + * @param {string} [config.server] - server 参数 + * @param {Function} [config.onSave] - 保存成功后的回调 + */ + async showEditDialog(config) { + const { tableName, data, server, onSave } = config; + const svr = server || this.getServer(); + const extraParams = config.extraParams || {}; + + // 获取列配置 + let columns = config.columns; + if (!columns) { + const params = { ...extraParams }; + if (svr !== 'gvsds-master') params.server = svr; + const qs = new URLSearchParams(params).toString(); + const resp = await fetch(`/API/FormAPI/Columns/${tableName}?${qs}`, { credentials: 'include' }); + const result = await resp.json(); + if (result.code !== 0) { + alert('获取列配置失败: ' + (result.msg || '未知错误')); + return; + } + columns = result.data || []; + } + + // 隐藏不可编辑字段 + const pkFieldMap = { + Company: 'CompanyUUID', + Department: 'DeptUUID', + Workgroup: 'WorkgroupUUID', + UserRole: 'UserRoleUUID', + User: 'UserUUID', + }; + const pkField = pkFieldMap[tableName] || (tableName + 'UUID'); + columns = columns.map(c => { + if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; } + if (c.field === pkField) { c.editable = false; c.hideInForm = true; } + if (c.field === 'UserPassword') { c.editable = false; c.hideInForm = true; } + return c; + }); + + // 生成表单 + const formHtml = new FormEngine().GenerateForm(columns, data); + + const pkValue = data[pkField]; + + // 保存对话框引用 + let dialogEl = null; + + window.GERPUI.ShowDialog({ + title: '编辑' + (data._displayName || ''), + content: formHtml, + width: '600px', + buttons: [ + { text: '取消', type: 'Secondary' }, + { + text: '保存', type: 'Primary', onClick: () => { + // 使用 FormEngine.GetFormData 收集表单数据 + if (!dialogEl) dialogEl = document.querySelector('.GERPDialog'); + const formEl = dialogEl ? dialogEl.querySelector('.GERPForm') : null; + if (!formEl) return false; + + const formData = new FormEngine().GetFormData(formEl); + // 移除不可编辑字段 + delete formData.CompanyUUID; + + // 调用 ModifyItem 接口 + const params = { ...extraParams }; + if (svr !== 'gvsds-master') params.server = svr; + const qs = new URLSearchParams(params).toString(); + const url = `/API/FormAPI/ModifyItem/${tableName}/${pkValue}?${qs}`; + + fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(formData), + }) + .then(resp => resp.json()) + .then(result => { + if (result.code === 0) { + window.GERPUI.CloseDialog(); + if (typeof onSave === 'function') onSave(); + } else { + alert('保存失败: ' + (result.msg || '未知错误')); + } + }) + .catch(err => { + alert('网络错误: ' + err.message); + }); + + return false; // 不自动关闭对话框 + } + } + ] + }); + }, +}; + +// ==================== TablePage 基类 ==================== +/** + * 强公司隔离的 TableEngine 页面基类。 + * 自动处理:server 参数、列配置获取、CompanyUUID 隐藏、标准 TableEngine 选项。 + * 子类只需提供 tableName、defaultColumns 和 onRowClick 回调。 + */ +class CompanyTablePage { + /** + * @param {Object} config + * @param {string} config.tableName - 表名(如 'Company', 'Department', 'Workgroup') + * @param {string} config.title - 页面标题 + * @param {Function} config.defaultColumns - 返回默认列数组的函数 + * @param {Function} [config.onView] - 查看按钮回调 (row) => void,设置后显示眼睛按钮 + * @param {string} [config.pkField] - 主键字段名,用于 fetchDetail 按字段过滤 + * @param {string} [config.container] - 容器选择器,默认 '#TableContainer' + */ + constructor(config) { + this.tableName = config.tableName; + this.title = config.title; + this.defaultColumnsFn = config.defaultColumns; + this.onView = config.onView || null; + this.pkField = config.pkField || (this.tableName + 'UUID'); + this.container = config.container || '#TableContainer'; + this.server = CompanyUtils.getServer(); + this.tableInstance = null; + this.drawerClose = null; + + CompanyUtils.setPageTitle(this.title); + } + + /** 构建带 server 的 FormAPI URL */ + apiUrl(path, params = {}) { + const allParams = {}; + if (this.server !== 'gvsds-master') allParams.server = this.server; + Object.assign(allParams, params); + const qs = new URLSearchParams(allParams).toString(); + return qs ? `${path}?${qs}` : path; + } + + /** 初始化表格 */ + async init() { + const containerEl = document.querySelector(this.container); + if (!containerEl) return; + + try { + let columns; + if (this.server === 'gvsds-master') { + const resp = await fetch(this.apiUrl(`/API/FormAPI/Columns/${this.tableName}`), { credentials: 'include' }); + const result = await resp.json(); + if (result.code !== 0) { + containerEl.innerHTML = `
加载列配置失败: ${CompanyUtils.esc(result.msg || '')}
`; + return; + } + columns = result.data || []; + } else { + columns = this.defaultColumnsFn(); + } + + // 强公司隔离:CompanyUUID 由后端自动注入,前端隐藏输入框 + columns = columns.map(c => { + if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; } + return c; + }); + + this.tableInstance = new TableEngine({ + container: this.container, + tableName: this.tableName, + columns, + data: [], + pageSize: 20, + showPagination: true, + showCheckbox: true, + editable: true, + sortable: true, + filterable: true, + striped: true, + hoverable: true, + bordered: true, + api: { + get: this.apiUrl(`/API/FormAPI/Get/${this.tableName}`), + add: this.apiUrl(`/API/FormAPI/Add/${this.tableName}`), + edit: this.apiUrl(`/API/FormAPI/Modify/${this.tableName}/`), + modify: this.apiUrl(`/API/FormAPI/ModifyItem/${this.tableName}/`), + delete: this.apiUrl(`/API/FormAPI/Delete/${this.tableName}/`), + }, + onView: this.onView, + // 注意:不设置 onEdit/onDelete,让 TableEngine 使用内置的编辑/删除对话框 + }); + } catch (error) { + console.error('init error:', error); + containerEl.innerHTML = `
初始化失败: ${CompanyUtils.esc(error.message)}
`; + } + } + + /** 通过 PK 字段获取单条记录 */ + async fetchDetail(pkValue, pageSize = 1) { + const params = { PageSize: String(pageSize) }; + params[this.pkField] = pkValue; + const url = this.apiUrl(`/API/FormAPI/Get/${this.tableName}`, params); + const resp = await fetch(url, { credentials: 'include' }); + return resp.json(); + } + + /** 通过 FormAPI 获取关联列表(支持字段过滤或关键字搜索) */ + async fetchRelated(relatedTable, filterValue, pageSize = 50, filterField = null) { + const params = { PageSize: String(pageSize) }; + if (filterField) { + params[filterField] = filterValue; + } else { + params.keyword = filterValue; + } + const url = this.apiUrl(`/API/FormAPI/Get/${relatedTable}`, params); + const resp = await fetch(url, { credentials: 'include' }); + return resp.json(); + } +} + +// 暴露到全局 +window.CompanyUtils = CompanyUtils; +window.CompanyTablePage = CompanyTablePage; diff --git a/TempLate/GERP/assets/gerp/GERPUI/GERPUI.js b/TempLate/GERP/assets/gerp/GERPUI/GERPUI.js index f94435fe..ca582d15 100644 --- a/TempLate/GERP/assets/gerp/GERPUI/GERPUI.js +++ b/TempLate/GERP/assets/gerp/GERPUI/GERPUI.js @@ -3194,6 +3194,13 @@ class GERPUI { }); }); + // 桌面端:标题栏可拖动 + const dlgHeader = dialog.querySelector('.GERPDialogHeader'); + const dlgContent = dialog.querySelector('.GERPDialogContent'); + if (dlgHeader && dlgContent) { + this._makeDraggable(dlgContent, dlgHeader); + } + return this.State.CurrentDialog; } @@ -3264,12 +3271,17 @@ class GERPUI { OpenDrawer(options) { const { title, content, placement = 'right', width = '400px', showClose = true, onClose, footer } = options; + const isMobile = this._isMobile(); const drawer = document.createElement('div'); drawer.className = `GERPDrawer GERPDrawer${this.Capitalize(placement)}`; + + // 手机端:全屏显示 + const actualWidth = isMobile ? '100%' : width; + drawer.innerHTML = `
-
+
${title ? `

${title}

@@ -3298,6 +3310,15 @@ class GERPUI { drawer.querySelector('.GERPDrawerMask')?.addEventListener('click', closeDrawer); drawer.querySelector('.GERPDrawerClose')?.addEventListener('click', closeDrawer); + // 桌面端:标题栏可拖动 + if (!isMobile) { + const header = drawer.querySelector('.GERPDrawerHeader'); + const contentEl = drawer.querySelector('.GERPDrawerContent'); + if (header && contentEl) { + this._makeDraggable(contentEl, header); + } + } + return { element: drawer, close: closeDrawer }; } @@ -3425,6 +3446,82 @@ class GERPUI { return str.charAt(0).toUpperCase() + str.slice(1); } + /** 判断是否为移动端 */ + _isMobile() { + return window.matchMedia('(max-width: 768px)').matches || 'ontouchstart' in window; + } + + /** + * 使元素可通过拖柄拖动(桌面端)。 + * 拖动时自动从 flex 居中切换到 absolute 定位,保持窗口在视口内。 + */ + _makeDraggable(contentEl, handleEl) { + if (this._isMobile()) return; // 手机端不启用拖动 + + let isDragging = false; + let startX, startY, startLeft, startTop; + let switchedToAbsolute = false; + + handleEl.style.cursor = 'move'; + handleEl.style.userSelect = 'none'; + + const onPointerDown = (e) => { + // 排除关闭按钮等子元素 + if (e.target.closest('.GERPDialogClose, .GERPDrawerClose, button')) return; + isDragging = true; + startX = e.clientX; + startY = e.clientY; + + if (!switchedToAbsolute) { + // 首次拖动:从 flex 居中切换到 absolute 定位 + const rect = contentEl.getBoundingClientRect(); + const parentEl = contentEl.parentElement; + // 取消父级 flex 居中 + parentEl.style.display = 'block'; + contentEl.style.position = 'absolute'; + contentEl.style.left = rect.left + 'px'; + contentEl.style.top = rect.top + 'px'; + contentEl.style.margin = '0'; + startLeft = rect.left; + startTop = rect.top; + switchedToAbsolute = true; + } else { + startLeft = parseFloat(contentEl.style.left) || 0; + startTop = parseFloat(contentEl.style.top) || 0; + } + + document.addEventListener('mousemove', onPointerMove); + document.addEventListener('mouseup', onPointerUp); + e.preventDefault(); + }; + + const onPointerMove = (e) => { + if (!isDragging) return; + const dx = e.clientX - startX; + const dy = e.clientY - startY; + let newLeft = startLeft + dx; + let newTop = startTop + dy; + + // 限制在视口内 + const rect = contentEl.getBoundingClientRect(); + const vw = window.innerWidth; + const vh = window.innerHeight; + newLeft = Math.max(0, Math.min(newLeft, vw - rect.width)); + newTop = Math.max(0, Math.min(newTop, vh - rect.height)); + + contentEl.style.left = newLeft + 'px'; + contentEl.style.top = newTop + 'px'; + }; + + const onPointerUp = () => { + isDragging = false; + document.removeEventListener('mousemove', onPointerMove); + document.removeEventListener('mouseup', onPointerUp); + }; + + handleEl.addEventListener('mousedown', onPointerDown); + } + // ==================== 高级模态框 ==================== ShowModal(options) { const { title, content, width = '520px', closable = true, maskClosable = true, footer = null, onOk, onCancel, className = '' } = options; @@ -3490,6 +3587,13 @@ class GERPUI { closeModal(); }); + // 桌面端:标题栏可拖动 + const modalHeader = modal.querySelector('.GERPModalHeader'); + const modalWrapper = modal.querySelector('.GERPModalWrapper'); + if (modalHeader && modalWrapper) { + this._makeDraggable(modalWrapper, modalHeader); + } + return { element: modal, close: closeModal }; } diff --git a/TempLate/GERP/assets/gerp/TableEngine.js b/TempLate/GERP/assets/gerp/TableEngine.js index d507b55e..b4ea9e04 100644 --- a/TempLate/GERP/assets/gerp/TableEngine.js +++ b/TempLate/GERP/assets/gerp/TableEngine.js @@ -343,9 +343,10 @@ class TableEngine { const cell = document.createElement('div'); cell.className = 'GERPTableHeaderCell GERPTableActionsCell GERPTableFixedRight'; cell.textContent = '操作'; - cell.style.flex = '0 0 100px'; - cell.style.width = '100px'; - cell.style.minWidth = '100px'; + const w = this.Options.onView ? '150px' : '100px'; + cell.style.flex = `0 0 ${w}`; + cell.style.width = w; + cell.style.minWidth = w; cell.style.justifyContent = 'center'; return cell; } @@ -465,20 +466,42 @@ class TableEngine { _createActionsCell(row) { const cell = document.createElement('div'); cell.className = 'GERPTableCell GERPTableActionsCell GERPTableFixedRight'; - cell.style.flex = '0 0 100px'; - cell.style.width = '100px'; - cell.style.minWidth = '100px'; + const hasView = !!this.Options.onView; + const w = hasView ? '150px' : '100px'; + cell.style.flex = `0 0 ${w}`; + cell.style.width = w; + cell.style.minWidth = w; cell.style.justifyContent = 'center'; const actions = document.createElement('div'); actions.className = 'GERPTableActions'; + // 图标按钮通用样式(紧凑型) + const iconBtnStyle = 'padding:4px 8px;font-size:13px;'; + + // 查看按钮(眼睛图标) + if (hasView) { + const viewBtn = document.createElement('button'); + viewBtn.className = 'GERPBtn Text'; + viewBtn.innerHTML = ''; + viewBtn.title = '查看'; + viewBtn.style.cssText = iconBtnStyle + 'color:var(--PrimaryColor);'; + viewBtn.addEventListener('click', (e) => { + e.stopPropagation(); + console.log('[TableEngine] onView clicked, row:', row); + this.Options.onView(row); + }); + actions.appendChild(viewBtn); + } + if (this.Options.editable) { const editBtn = document.createElement('button'); editBtn.className = 'GERPBtn Text'; editBtn.innerHTML = ''; editBtn.title = '编辑'; - editBtn.addEventListener('click', () => { + editBtn.style.cssText = iconBtnStyle; + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); this.ShowEditDialog(row); }); actions.appendChild(editBtn); @@ -488,7 +511,11 @@ class TableEngine { deleteBtn.className = 'GERPBtn Text'; deleteBtn.innerHTML = ''; deleteBtn.title = '删除'; - deleteBtn.addEventListener('click', () => this.HandleDelete(row)); + deleteBtn.style.cssText = iconBtnStyle; + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.HandleDelete(row); + }); actions.appendChild(deleteBtn); cell.appendChild(actions); @@ -684,14 +711,9 @@ class TableEngine { if (column.type === 'entityPicker' && column.entityPicker) { const ep = column.entityPicker; - // 尝试从行数据中获取关联实体的显示名称 - const labelField = ep.labelField || ''; - const valueField = ep.valueField || column.field; - // 查找行中是否有 "字段名_标签" 或 "字段名__label" 等辅助字段 + // 优先使用后端附带的 _label 字段(关联实体的实际名称) let label = ''; - if (row[labelField]) { - label = String(row[labelField]); - } else if (row[column.field + '_label']) { + if (row[column.field + '_label']) { label = String(row[column.field + '_label']); } else if (row[column.field + '__label']) { label = String(row[column.field + '__label']); @@ -808,20 +830,21 @@ class TableEngine { return; } - const checkbox = e.target.closest('.GERPTableRowCheckbox'); - if (checkbox) { - const rowId = checkbox.dataset.id; - this.ToggleRowSelection(rowId); - } + const checkboxLabel = e.target.closest('.GERPCheckbox'); + if (checkboxLabel) { + const input = checkboxLabel.querySelector('input'); + if (input?.classList.contains('GERPTableRowCheckbox')) { + const rowId = input.dataset.id; + this.ToggleRowSelection(rowId); + } - const selectAll = e.target.closest('.GERPTableSelectAll'); - if (selectAll) { - const isChecked = selectAll.checked; - if (selectAll.indeterminate) { - selectAll.checked = false; - this.ToggleSelectAll(false); - } else { - this.ToggleSelectAll(isChecked); + if (input?.classList.contains('GERPTableSelectAll')) { + if (input.indeterminate) { + input.checked = false; + this.ToggleSelectAll(false); + } else { + this.ToggleSelectAll(input.checked); + } } } @@ -1529,7 +1552,7 @@ class TableEngine { cell.classList.add('GERPTableCellEditing'); - const input = this._createEditInput(value, column); + const input = this._createEditInput(value, column, row); cell.innerHTML = ''; cell.appendChild(input); @@ -1557,7 +1580,7 @@ class TableEngine { if (input.select) input.select(); } - _createEditInput(value, column) { + _createEditInput(value, column, rowData) { let input; switch (column.type) { @@ -1636,9 +1659,14 @@ class TableEngine { input.className = 'GERPEntityPicker'; const ep = column.entityPicker || {}; const epValue = value || ''; + // 从行数据中获取关联实体的显示名称 + const epLabel = (rowData && rowData[column.field + '_label']) + || (rowData && rowData[column.field + '__label']) + || ''; + const epDisplay = epLabel || (epValue ? String(epValue).substring(0, 8) + '...' : ''); input.innerHTML = ` -
${epValue ? String(epValue).substring(0, 8) + '...' : ''}
+
${this._escapeHtml(epDisplay)}
`; input.style.position = 'relative'; diff --git a/TempLate/GERP/assets/gerp/gerp.js b/TempLate/GERP/assets/gerp/gerp.js index cea46151..aef09dd3 100644 --- a/TempLate/GERP/assets/gerp/gerp.js +++ b/TempLate/GERP/assets/gerp/gerp.js @@ -1000,11 +1000,17 @@ class GERP { // 静态方法:供子页面调用 static Navigate(page, title) { - window.parent.postMessage({ type: 'NAVIGATE', data: { page, title } }, '*'); + if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) { + // 在 GERP 父框架内:通过 postMessage 通知父框架打开新标签页 + window.parent.postMessage({ type: 'NAVIGATE', data: { page, title } }, '*'); + } else { + // 非 GERP 父情况:在新标签页打开 + window.open(page, '_blank'); + } } static SetTitle(title) { - if (window.parent !== window) { + if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) { window.parent.postMessage({ type: 'SET_TITLE', data: { title } }, '*'); } else { document.title = title; @@ -1012,7 +1018,11 @@ class GERP { } static Refresh() { - window.parent.postMessage({ type: 'REFRESH' }, '*'); + if (window.parent !== window && window.parent.GERPApp && window.parent.GERPApp.UI) { + window.parent.postMessage({ type: 'REFRESH' }, '*'); + } else { + window.location.reload(); + } } } diff --git a/TempLate/GERP/pages/Company/company-detail.html b/TempLate/GERP/pages/Company/company-detail.html new file mode 100644 index 00000000..320c90ec --- /dev/null +++ b/TempLate/GERP/pages/Company/company-detail.html @@ -0,0 +1,244 @@ + + +
+
+
+ 返回 +

+ + 公司详情 +

+ +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/company.html b/TempLate/GERP/pages/Company/company.html new file mode 100644 index 00000000..dc4e0573 --- /dev/null +++ b/TempLate/GERP/pages/Company/company.html @@ -0,0 +1,38 @@ + + +
+
+
+

+ + 公司管理 +

+
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/department-detail.html b/TempLate/GERP/pages/Company/department-detail.html new file mode 100644 index 00000000..d3622ba2 --- /dev/null +++ b/TempLate/GERP/pages/Company/department-detail.html @@ -0,0 +1,152 @@ + + +
+
+
+ 返回 +

+ + 部门详情 +

+ +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/department.html b/TempLate/GERP/pages/Company/department.html new file mode 100644 index 00000000..b05fcc61 --- /dev/null +++ b/TempLate/GERP/pages/Company/department.html @@ -0,0 +1,39 @@ + + +
+
+
+

+ + 部门管理 +

+
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/employee-detail.html b/TempLate/GERP/pages/Company/employee-detail.html new file mode 100644 index 00000000..22d3a662 --- /dev/null +++ b/TempLate/GERP/pages/Company/employee-detail.html @@ -0,0 +1,187 @@ + + +
+
+
+ 返回 +

+ + 员工详情 +

+ +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/workgroup-detail.html b/TempLate/GERP/pages/Company/workgroup-detail.html new file mode 100644 index 00000000..eef482ac --- /dev/null +++ b/TempLate/GERP/pages/Company/workgroup-detail.html @@ -0,0 +1,145 @@ + + +
+
+
+ 返回 +

+ + 工作组详情 +

+ +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/workgroup.html b/TempLate/GERP/pages/Company/workgroup.html new file mode 100644 index 00000000..3669aa27 --- /dev/null +++ b/TempLate/GERP/pages/Company/workgroup.html @@ -0,0 +1,35 @@ + + +
+
+
+

+ + 工作组管理 +

+
+
+
+
+ + diff --git a/TempLate/GERP/pages/system/role-detail.html b/TempLate/GERP/pages/system/role-detail.html new file mode 100644 index 00000000..d6769e38 --- /dev/null +++ b/TempLate/GERP/pages/system/role-detail.html @@ -0,0 +1,261 @@ + + +
+
+
+ 返回 +

+ + 角色详情 +

+ + +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/system/roles.html b/TempLate/GERP/pages/system/roles.html new file mode 100644 index 00000000..3f4b5753 --- /dev/null +++ b/TempLate/GERP/pages/system/roles.html @@ -0,0 +1,118 @@ + + +
+
+
+

+ + 角色管理 +

+
+ +
+
+
+ + diff --git a/TempLate/GERP/pages/system/user-detail.html b/TempLate/GERP/pages/system/user-detail.html new file mode 100644 index 00000000..7dfe5824 --- /dev/null +++ b/TempLate/GERP/pages/system/user-detail.html @@ -0,0 +1,227 @@ + + +
+
+
+ 返回 +

+ + 用户详情 +

+ + +
+
+
加载中...
+
+
+
+ + diff --git a/TempLate/GERP/pages/system/users.html b/TempLate/GERP/pages/system/users.html index 125e3f15..c67db1d8 100644 --- a/TempLate/GERP/pages/system/users.html +++ b/TempLate/GERP/pages/system/users.html @@ -4,24 +4,6 @@ .user-header { display: flex; align-items: center; margin-bottom: 16px; gap: 12px; flex-shrink: 0; } .user-header h1 { margin: 0; flex: 1; } .user-container { flex: 1; min-height: 0; overflow: hidden; display: flex; flex-direction: column; } - - /* 详情面板内样式 */ - .user-detail-section { margin-bottom: 20px; } - .user-detail-section h3 { font-size: 14px; font-weight: 600; margin: 0 0 10px 0; padding-bottom: 8px; border-bottom: 1px solid var(--BorderColor); display: flex; align-items: center; gap: 8px; } - .user-detail-section h3 i { color: var(--PrimaryColor); } - .user-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 16px; } - .user-info-item { font-size: 13px; } - .user-info-item .label { color: var(--TextSecondary); font-size: 12px; } - .user-info-item .value { color: var(--TextPrimary); font-weight: 500; word-break: break-all; } - .user-detail-list { list-style: none; padding: 0; margin: 0; } - .user-detail-list li { padding: 8px 12px; border: 1px solid var(--BorderColor); border-radius: 6px; margin-bottom: 6px; font-size: 13px; display: flex; align-items: center; gap: 8px; } - .user-detail-list li .item-name { font-weight: 500; flex: 1; } - .user-detail-list li .item-desc { color: var(--TextSecondary); font-size: 12px; } - .user-order-item { display: flex; align-items: center; gap: 12px; width: 100%; } - .user-order-item .order-no { font-weight: 600; color: var(--PrimaryColor); } - .user-order-item .order-amount { font-weight: 600; } - .user-order-item .order-status { margin-left: auto; } - .user-empty { text-align: center; padding: 20px; color: var(--TextSecondary); font-size: 13px; }
@@ -40,9 +22,7 @@ diff --git a/TempLate/login.html b/TempLate/login.html index bbfdd9c1..b3551600 100644 --- a/TempLate/login.html +++ b/TempLate/login.html @@ -1,5 +1,8 @@
-
Card image +
+ Card image

登录

-
+ +
+
-
+ +
+
-
+ +
+
-
-
+
+ +
+
+ +
-
我还没有账号,去 注册
-
+
+ 我还没有账号,去 + 注册 +
+
+ +
@@ -51,10 +77,99 @@ if (window.parent && window.parent !== window) {window.parent.location.reload(); @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} .animate-fade-in{animation:fadeIn .3s ease-out forwards} input:focus~.input-icon{color:#a855f7} -#LoginToast{animation:fadeIn .3s ease-out forwards} +#LoginToast{ + animation:fadeIn .3s ease-out forwards; + position:relative; + z-index:999; + width:100%; + box-sizing:border-box; +} \ No newline at end of file diff --git a/TempLate/register.html b/TempLate/register.html index 9423526f..1875b4a0 100644 --- a/TempLate/register.html +++ b/TempLate/register.html @@ -16,37 +16,60 @@ if (window.parent && window.parent !== window) { window.parent.location.reload()
-
-
Card image +
+
+ Card image

注册

-
+ +
+
-
+ +
+
-
+
+ +
+
-
+ +
+
-
+ +
+
-
-
+
+ +
+
+ +
-
我已有账号,去 登录
-
+
我已有账号,去 + 登录 +
+
+ +
@@ -60,8 +83,98 @@ if (window.parent && window.parent !== window) { window.parent.location.reload() @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} .animate-fade-in{animation:fadeIn .3s ease-out forwards} input:focus~.input-icon{color:#a855f7} -#RegisterToast{animation:fadeIn .3s ease-out forwards} +#RegisterToast{ + animation:fadeIn .3s ease-out forwards; + position:relative; + z-index:999; + width:100%; + box-sizing:border-box; +} \ No newline at end of file diff --git a/gvsds/__pycache__/model_base.cpython-312.pyc b/gvsds/__pycache__/model_base.cpython-312.pyc index ad5e44ada3ae60b05c68d25dbec29d87055a28ed..2e64fc23a8971063664e69580cf67e7950134f9f 100644 GIT binary patch delta 102 zcmeBSInK;`nwOW00SNYUm}YrR>J%-MCgVdw|~rtb_e;sc0PBmh(d0Lo1k`Tzg` delta 188 zcmX@k+{40qnwOW00SGMobg~*J@=7wgO;op)%H@vY&gF^XVPvRe)?|4JQlQCri&0CH zXJVP6k)&H@YEFu4ML|(&adBpTo=a+Ra#3bMNq*5SF}M7p)Xele@6<}q6omN1XI}YPhl_Fd$n(&RjSpUeAx z-*?VwFP~#3K4%JU=I7@q(C6%z4RK>_vcRA`R`!*{Y=krVLJ*9_@V%)DjvI@aZ$PVd z!Edvb)ik5^%~oHa>LSTKn62vgB2`hGsz>H({iDge2^C&hp-3`Hg{X@CEgIsNAk44P zKYq#khj?S!70eUpEwwV2KsKy^+XZ>>b!jnk1$O~s#ud$Y%*$muu6r(Cyg*z(KW=y~ zUa~;kNaE<&^=LCDe7$f2etuJTRE(mBtVX#~F$bYpWa2lV+iJ_T%<>-*x2RRfT?&&{ z+Y;;CglD~FSTC{7>3!^IzCzI*9cSJvcPzVLE(Qpn!>*Ur0;rm-B{+7UQ>ZmZP07BLxkf5p zBe82`jR3DV>8YECU1M@%s&tj4TP?9Qa=riqGp_cf-bDg@I^z~$wchUZsJ`gK`P;qc{iP{aC+7@UGv5YQEKtu(uWO z&KvfWL zVsj8Td=$jXUk>6W{|e$`YeRUMT7taiJl77t`SC=^}na zDK}HUh5Fk&t(>1Kt?+TDgKMKC0N-{l=Yo`k;9h4r*G@?oj9qrFlaelI?Q(GuO4h@n pu5xYzC2zq*mxJ3xNjD*HQ_=&u5m(M;LdJ6CZQzRxvyaiApy9=Pok~A2P*XX*4Ps`dw8w4z+qR#RmDG_Nz|b1QM%+y(S&=DU=kr;nF%=US~VZ zI_4U2hu2wlovl)4@i7LtVsgS6r52_v6;xHHrcx_|%p8N4mdR3oe_S-DpghCKWobp~ zpEPZI&#Kk``%G}|%6USpSGA6jfJ6+REio;K;|6O^0Q%EZ}FN|uc^m>aLN%f|dqAgl9 zq@}iY3gwG8PK}gyi*!la0x?^i6WOJYgv+)Fmv4kC9tm&TBCJbW&fAotH^JekVk~gNQB`i;=zrMBsBk?9cGDoG*0T4yCAfVInxu%k;)l6O_I0_AB_rax!hDQ%#YowGEG6-^?$Q_>6YAn%&6 ztIOzfMweLX7THG0EP&hla92Eb`3w4-y-hT>i|lU6CctxE1KqO&yOzvo&gK#IyF|7{ zsuEz@>p*73F59u5a&k-UL<%U=t32V zaD6v+pC*_lc!pq>;12|uo(7b0_u%9{f+u_M{L7^N9l^qo8&y1^E`(ztypvBtc$J?B zYQmWB4&(Yr7|*##@CLzO32OJ?x@QkwD@E`;!RrKnCuoUaHypvMj}rbt1h4yT1kYy$ zeBL?%&xsQpBREU&Cc)nbRzz{8J=#FOMGQKlHHtX8rZZ7I=w=izbB~0~y-kP<1Sfm( z0Y2+>(YEJt8#kaJ-!lb=W4tR(#X$wSB{`+_orxt|)1s|u$=14PYZYzn*K2RF5$O<3 zy#$}fcy5r2A@~`Im#KIK3gbLCOvPcSkMr&$RJ=+j*MBs$Dth2RvYHD~5rH?8HJm_26y}p|u9u1!{DER|D*C|O=j8TMv5$y;DxQJkeQwpW SM4VDf2jE)Y2>TcMlll+DfD6C? diff --git a/gvsds/apps/commission/__pycache__/manager.cpython-312.pyc b/gvsds/apps/commission/__pycache__/manager.cpython-312.pyc index 2a88c8df542ed1d38fc3ca3f57aebc913ac5245c..1c22192c1294c9ed3297241d73b836d2f8dab92d 100644 GIT binary patch delta 29 jcmbO>lX2QiM()$Ryj%=G;AUd9k^3|=qs!*&%=lX2QiM()$Ryj%=GaP7FkM()$hj47M1Gw=5R0Eg8Hs{jB1 diff --git a/gvsds/apps/core/__pycache__/models.cpython-312.pyc b/gvsds/apps/core/__pycache__/models.cpython-312.pyc index 0d0d9d76996927099d2d5b3acdc51adb0173c48f..91cd90199e664f7de71b009898d2a9c135f27f93 100644 GIT binary patch delta 1344 zcmZ9KZA@Eb6vyv#%Ps9~3oYe#&;b^dciMHeQp#u`Kr66<$eS~>X&AlJlJTG3J;?<8xY=#GCYY&+|X$ z`QLM%r{8=+Ru)L!4UL9p=ySzk8-MqiFY>Ojq%(GdZLHxHRF%=+{bf+qkICn`2sfS& zqs{v2oB+2ZUu1C`!_3RWx@^QSVf}uzU;AY4!P-TV&F{bGb0e~G1>9@a!?F58y?jaW zIHQQieS8DanP(J3P&pjuKdV)yhJ7vOHEF=vpe)qtmA z*OJcbn3|X#otzsU?(Xyl1GOZbKQMJh+*|tn!AU9=>=l8>+(}gMzS&wRwNSUTEF^~0 za`9X+6b{w$uwpiO_?eI>`6cQRO6Nb0{WCMcKq##l%YJQd$w|}xv!OIUElLwvDTEhb zGMPYbvA=$c#&0kW)r_*A#1+q_gwmu?85b&(LPK0=ND0N4eV2S&GR9!O&#(qQ%5TY; z!<9*+HEy&nmwr36YP2SdJ<!)-izKuVSCh_5{yZqDlSwl z%wM&y3RMZAEy}0zi{VSl$7CD&kGx0zfSX4?B!9xR^&I&Jez(pjS$gNvFLWy4ZHM8_ zqfP=kD{0_}ah!SwSJ|jD^yDmLZ zJw$aE@d(tdK~_d!w)JT)mrmaZpSSu{_!;SM#5x>mtKemLE(i;Bw4KEIHD^)mYTH*t z_W-LdAZ8Ic1YT=*(WAZAK0?%>@38ZEbYB-5C}PidJV8hwobCL9)e|T;>0!`qA_EY1 zi)08(f{USYhzNbW=IYO0p>gC|%V0bh<6QVOadz$z%^hUe@ z7kWNr%Lr_`is0DsT@{C!?8xw4dkdWR7Lhstubpe4nQ8dm>mgg9@2#}0W0Uy^ErQ_2 z%tZg2qYe{@AdL6=sM+=2Im;GqIfNKMOdvA>MJES_@Hknub(B> PXyXop@14`I+2{QiO)+}F delta 1086 zcmZvaZ%h++7{~89ylZ=TQEGW3Oi))=Lyf+Bitoy@5- zYQ{{Kj680}B^vi;F@ZNrqsit>mMwcTH)l1jr1GLK6r+h)M1y~~{hs6PF1gR|d(Xe$ zbI+e&QjXzRuTG;*DYx)S+4F;|HwxL8Bc%Q%XR< zyrNiyal;r;7RSx|tj4%7E{^f@wj(0$tX6E|ClEJ{OJhaz)&U!amX(7Uf3ALEtb&}i zcWAGQQ#Gw%LdENoa-w>uKaosS8F0w!a$8dgRokYqm~E+4e>{rK0j`vPA8;G4j1$f8;sgazquYE|(3PToHVHwZTg zw+MF#6NFrP>8jbAs5`m=1`@i8wf^B zgboU%`bKSXy6C+Y9;UIqezowN*(&fveav>3shnwp+NY_>PIXwD#F_d6n3Q3;> zp$a3-TLn^jPro!@2HO*gJ52b9@DGP0E%jo7^a1pDtV2Vq3?i1d1|=igp@qE~pWfQ~ z4nPdvjJ3HSf=Am_*8jb>3eik~lUUqdRbwT2f+|`_l@d15r%bRDM6waq z;Ewhy0x7-4!274bK`lO9&X6)PJUDmt{4-9KH#k1P31P2ntJBUJQTi z*zD6sYv>x<)xR&?le6feXYcmp&LM43n`C8jX5F?SxjmUmutBwSu8|zn1)&#Db$$nq zbaj^vXoto~3_N%~5)_s(?;iX+(gu&Py!%~yo|V$9gi^w52J|(^l7~tW;+X8Fs4!*iUezzQdr6ui+EF5 zYuQS;L5e}3hHW(?gcZyHRK*HZ#g?tXz)+-wP?N%rqJ|5mrWmA}1FV`GNi`>mYM>gR zk&`C~%Sv*=B#QVD%4=9sxF#PHR+Z$2$u{zp@IssfbRJ6zH%NB!T4Anw<`j-vraZP1 zHkfP^V+~UlKa7W6Pu z!Ww2^IPg^gxvQBVVzulw>_t2!!f+MrHOw%%UC)Ej*De?yqzChv^hfQvNN@-52UD1Ti>m^q( Q@#`{*O>p_j0HnaG04rW?&j0`b delta 705 zcmZ2)@yvqvG%qg~0}!NW=wvp>R<)x;KDnby(0)t!-u0*y(6ed%{3=9u}Dj;_?6GW_*qlTl1r$ijCf}@5RCKt?5B0Bkl zh^RJa4JV2kP82mrp41eYY#?eTTLdy(lkpaNQEFLgQE_UOV@63yL9vy-erbB{*ZCdcIz1QBHADJxHD%M6iGq zair#@6qjd~WE3TV#6X_XJq3ncb2c2AC%lwh2+xk7RU6Tcp#*aVla3_z;LAE*rgtWbeN diff --git a/gvsds/apps/core/models.py b/gvsds/apps/core/models.py index 1b5112cd..56eec088 100644 --- a/gvsds/apps/core/models.py +++ b/gvsds/apps/core/models.py @@ -221,12 +221,17 @@ class User(QModel): 汇总所有角色的操作权限后判断。 """ from gvsds.apps.gerp.models import Permission, RolePermission, UserRole - user_role_uuids = UserRole.objects.filter( + user_roles = UserRole.objects.filter( UserUUID=self.UserUUID - ).values_list('RoleUUID_id', flat=True) - perm_uuids = RolePermission.objects.filter( + ) + user_role_uuids = user_roles.values_list('RoleUUID_id', flat=True) + company_uuids = list(set(user_roles.values_list('CompanyUUID_id', flat=True))) + rp_qs = RolePermission.objects.filter( RoleUUID_id__in=user_role_uuids - ).values_list('PermUUID_id', flat=True) + ) + if company_uuids: + rp_qs = rp_qs.filter(CompanyUUID_id__in=company_uuids) + perm_uuids = rp_qs.values_list('PermUUID_id', flat=True) return Permission.objects.filter( PermUUID__in=perm_uuids, PermCode=perm_code, diff --git a/gvsds/apps/core/views.py b/gvsds/apps/core/views.py index 3506810e..1ca13abc 100644 --- a/gvsds/apps/core/views.py +++ b/gvsds/apps/core/views.py @@ -136,13 +136,12 @@ class SubdomainView(View): if subdomain == 'ugm': elt_token = request.COOKIES.get('ELToken') if not elt_token: - # 未登录 → 跳转登录页 + # 未登录 → 跳转 www 登录页 if file_path and file_path not in ('/', 'login.html', 'register.html'): - from django.urls import reverse - login_url = f"https://ugm.gvsds.com/login.html?redirect=https://gerp.gvsds.com/" + login_url = f"https://www.gvsds.com/login.html?redirect=https://gerp.gvsds.com/" return HttpResponseRedirect(login_url) if not file_path or file_path == '/': - return HttpResponseRedirect('https://ugm.gvsds.com/login.html') + return HttpResponseRedirect('https://www.gvsds.com/login.html') else: # 已登录 → 跳转 gerp if not file_path or file_path == '/' or file_path == 'index.html': diff --git a/gvsds/apps/gerp/__pycache__/form_service.cpython-312.pyc b/gvsds/apps/gerp/__pycache__/form_service.cpython-312.pyc index e27f25898bd647d8ae80290bce07981a458235e5..36c12bb3b6f1d86561236da32dc490fbf377c48c 100644 GIT binary patch literal 36342 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 V1GJG&|JUkcO4@XnLP*xp{|10J)As-X delta 7827 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 b;-ANn@a@Mh`w!*}b%@44kIUhOPx|}^##B<; delta 8607 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 IU%%|}-@wK}N&o-= diff --git a/gvsds/apps/gerp/form_service.py b/gvsds/apps/gerp/form_service.py index 0f26e3a9..d8031e7c 100644 --- a/gvsds/apps/gerp/form_service.py +++ b/gvsds/apps/gerp/form_service.py @@ -2,6 +2,7 @@ import uuid import datetime from django.db import connection from django.db.models import Q +from django.db.models.fields import NOT_PROVIDED from gvsds.apps.gerp.models import ( Tenant, CompanyInfo, Company, Department, Role, UserRole, RolePermission, @@ -16,6 +17,61 @@ from gvsdsdk.order.models import Order, OrderItem, OrderLevel, OrderTemplate, Or from gvsds.apps.gerp.i18n import translate_field, translate_table +# 系统默认公司 UUID(GERP 后台) +SYSTEM_COMPANY_UUID = uuid.UUID('00000000-0000-0000-0000-000000000001').bytes + + +def create_default_role_for_user(user, admin_user=None): + """为新用户创建默认角色及用户-角色关联。 + + 角色名格式为"{用户名}的默认角色",状态为启用。 + 默认绑定系统公司(00000000-0000-0000-0000-000000000001), + 角色租户取自该系统公司所属租户,回退到管理员租户或系统首个租户。 + + Args: + user: 新创建的 User 实例 + admin_user: 可选,操作管理员 User 实例(用于租户回退) + """ + try: + company_uuid = SYSTEM_COMPANY_UUID + # 角色租户优先取自系统公司所属租户 + tenant_uuid = None + try: + company = Company.objects.get(CompanyUUID=company_uuid) + tenant_uuid = company.TenantUUID_id + except Company.DoesNotExist: + pass + # 回退:当前操作管理员的首个角色租户 + if not tenant_uuid and admin_user is not None: + admin_ur = UserRole.objects.filter( + UserUUID_id=admin_user.UserUUID + ).select_related('RoleUUID').first() + if admin_ur and admin_ur.RoleUUID_id and admin_ur.RoleUUID: + tenant_uuid = admin_ur.RoleUUID.TenantUUID_id + # 回退:系统首个租户 + if not tenant_uuid: + tenant = Tenant.objects.first() + if tenant: + tenant_uuid = tenant.TenantUUID + if not tenant_uuid: + return + user_name = user.UserName or '用户' + role = Role.objects.create( + RoleUUID=uuid.uuid4().bytes, + TenantUUID_id=tenant_uuid, + RoleName=f"{user_name}的默认角色", + RoleStatus=1, + ) + UserRole.objects.create( + UserRoleUUID=uuid.uuid4().bytes, + UserUUID_id=user.UserUUID, + RoleUUID_id=role.RoleUUID, + CompanyUUID_id=company_uuid, + ) + except Exception: + pass + + def _pascalize_record(record, model=None): result = {} column_map = {} @@ -87,8 +143,65 @@ class FormService: # 只读表:普通用户只能查询,不能增删改 READONLY_TABLES_FOR_NORMAL = {'Company', 'CompanyInfo', 'Tenant', 'Role', 'Password', 'EphemeralToken'} - def __init__(self, user): + def __init__(self, user, current_company_uuid=None, admin_verify=False, current_role_uuid=None): self.user = user + self.current_role_uuid = None + if current_role_uuid: + if hasattr(current_role_uuid, 'bytes'): + self.current_role_uuid = current_role_uuid.bytes + elif isinstance(current_role_uuid, str): + try: + self.current_role_uuid = uuid.UUID(current_role_uuid).bytes + except ValueError: + self.current_role_uuid = current_role_uuid + else: + self.current_role_uuid = current_role_uuid + # 当前角色所在公司的 UUID,统一转为 bytes 用于查询 + # 可能传入的类型:Company 实例、UUIDObj、uuid.UUID、bytes、str + if current_company_uuid is not None: + if hasattr(current_company_uuid, 'pk'): + # Company 模型实例 + self.current_company_uuid = current_company_uuid.pk + elif hasattr(current_company_uuid, 'bytes'): + # UUIDObj 或 uuid.UUID + self.current_company_uuid = current_company_uuid.bytes + elif isinstance(current_company_uuid, str): + try: + self.current_company_uuid = uuid.UUID(current_company_uuid).bytes + except ValueError: + self.current_company_uuid = current_company_uuid + else: + self.current_company_uuid = current_company_uuid + else: + self.current_company_uuid = None + # 强公司隔离:默认启用,仅当 admin_verify=True 且用户是管理员时才关闭 + if admin_verify and self._is_admin(user): + self.company_scope = False + else: + self.company_scope = True + import logging + logging.info('[FormService] __init__: user=%s, company_scope=%s, current_company_uuid=%s (type=%s)', + user, self.company_scope, + self.current_company_uuid.hex() if isinstance(self.current_company_uuid, bytes) else self.current_company_uuid, + type(self.current_company_uuid).__name__ if self.current_company_uuid else 'None') + + def _is_admin(self, user): + """判断用户是否为管理员:超级用户或当前角色拥有 admin:super 权限""" + if not user or not hasattr(user, 'is_authenticated') or not user.is_authenticated: + return False + # 超级用户始终是管理员 + if getattr(user, 'IsSuperuser', False): + return True + # 通过 RolePermission 判断:当前角色是否拥有 admin:super 权限 + if self.current_role_uuid: + try: + return RolePermission.objects.filter( + RoleUUID_id=self.current_role_uuid, + PermUUID__PermCode='admin:super' + ).exists() + except Exception: + pass + return False def _build_field_map(self, model): pascal_map = {} @@ -98,6 +211,80 @@ class FormService: pascal_map[f.name] = f return pascal_map + def _get_entity_picker_fields(self, model): + """获取模型中所有 entityPicker 类型的字段信息""" + result = [] + for f in model._meta.get_fields(): + if not hasattr(f, 'name'): + continue + # ForeignKey 字段 + if f.__class__.__name__ == 'ForeignKey': + try: + related_model = getattr(f, 'related_model', None) + if related_model: + related_name = related_model.__name__ + ep_config = self.ENTITY_PICKER_MAP.get(related_name) + if ep_config: + result.append({ + 'field_name': f.name, + 'label_field': ep_config['labelField'], + 'value_field': ep_config['valueField'], + 'entity_name': related_name, + 'extra_fields': ep_config.get('extraFields', []), + }) + except Exception: + pass + # UUIDField 字段(非 ForeignKey) + elif f.__class__.__name__ == 'UUIDField' and not getattr(f, 'primary_key', False): + entity_name = self.UUID_FIELD_ENTITY_MAP.get(f.name) + if entity_name: + ep_config = self.ENTITY_PICKER_MAP.get(entity_name) + if ep_config: + result.append({ + 'field_name': f.name, + 'label_field': ep_config['labelField'], + 'value_field': ep_config['valueField'], + 'entity_name': entity_name, + 'extra_fields': ep_config.get('extraFields', []), + }) + return result + + def _enrich_entity_labels(self, page_qs, ep_fields, records): + """为 records 中的 entityPicker 字段添加 _label 后缀字段,以及 extraFields""" + try: + obj_list = list(page_qs) + except Exception: + return records + for i, obj in enumerate(obj_list): + if i >= len(records): + break + for ep_info in ep_fields: + field_name = ep_info['field_name'] + label_field = ep_info['label_field'] + extra_fields = ep_info.get('extra_fields', []) + try: + related_obj = getattr(obj, field_name, None) + if related_obj is not None: + # 获取 db_column 作为 key 前缀 + db_col = None + for f in obj._meta.get_fields(): + if hasattr(f, 'name') and f.name == field_name and hasattr(f, 'db_column'): + db_col = f.db_column or f.name + break + key_prefix = db_col or field_name + # label 字段 + label = getattr(related_obj, label_field, None) + if label is not None: + records[i][key_prefix + '_label'] = str(label) + # extraFields 字段 + for ef in extra_fields: + ef_val = getattr(related_obj, ef, None) + if ef_val is not None: + records[i][key_prefix + '_' + ef] = ef_val + except Exception: + pass + return records + def _resolve_field_name(self, key, pascal_map): if key in pascal_map: return pascal_map[key].name @@ -107,6 +294,17 @@ class FormService: if val is None or val == '': if getattr(field, 'null', False): return None + # 非空字段:数值类型返回默认值或0,其他类型返回空字符串 + field_type = field.__class__.__name__ + if field_type in ('SmallIntegerField', 'IntegerField', 'BigIntegerField', + 'PositiveIntegerField', 'PositiveSmallIntegerField', + 'FloatField', 'DecimalField'): + default = getattr(field, 'default', None) + if default is not None and default is not NOT_PROVIDED: + return default + return 0 + if field_type == 'BooleanField': + return False return val field_type = field.__class__.__name__ if field_type == 'UUIDField' and isinstance(val, str): @@ -138,8 +336,16 @@ class FormService: return datetime.datetime.fromisoformat(val) except (ValueError, TypeError): return val - if hasattr(field, 'max_length') and isinstance(val, str) and len(val) > field.max_length: - val = val[:field.max_length] + if field_type in ('BooleanField', 'NullBooleanField') and isinstance(val, str): + low = val.strip().lower() + if low in ('true', '1', 't', 'yes'): + return True + if low in ('false', '0', 'f', 'no'): + return False + return val + max_length = getattr(field, 'max_length', None) + if max_length and isinstance(val, str) and len(val) > max_length: + val = val[:max_length] return val def get_tables(self): @@ -166,6 +372,7 @@ class FormService: 'labelField': 'RoleName', 'valueField': 'RoleUUID', 'placeholder': '输入角色名搜索...', + 'extraFields': ['RoleStatus'], }, 'User': { 'api': '/API/FormAPI/UserSearch', @@ -223,6 +430,12 @@ class FormService: 'valueField': 'MenuUUID', 'placeholder': '输入菜单名搜索...', }, + 'Workgroup': { + 'api': '/API/FormAPI/WorkgroupSearch', + 'labelField': 'WorkgroupName', + 'valueField': 'WorkgroupUUID', + 'placeholder': '输入工作组名搜索...', + }, } # UUID 字段名后缀到关联模型的映射(用于 UUIDField 非 ForeignKey 的场景) @@ -271,6 +484,10 @@ class FormService: 'primaryKey': is_pk, 'hideInForm': is_pk, } + # 强公司隔离:CompanyUUID 由后端自动注入,前端不可编辑 + if self.company_scope and f.name == 'CompanyUUID': + col['editable'] = False + col['hideInForm'] = True if field_type == 'select' and hasattr(f, 'choices') and f.choices: col['options'] = [{'value': k, 'label': str(v)} for k, v in f.choices] if field_type == 'number': @@ -358,6 +575,59 @@ class FormService: qs = model.objects.all() except Exception: return {'records': [], 'total': 0} + + # 强公司隔离:非管理员自动启用,仅返回当前公司数据 + if self.company_scope: + field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')} + if 'CompanyUUID' in field_names: + if not self.current_company_uuid: + # 无公司上下文时返回空结果 + import logging + logging.warning('[FormService] company_scope=True but current_company_uuid is None, user=%s', self.user) + return {'records': [], 'total': 0, 'page': 1, 'PageSize': 20} + # 判断 CompanyUUID 字段类型:ForeignKey 用 _id 后缀,UUIDField 直接用字段名 + company_field = None + for f in model._meta.get_fields(): + if hasattr(f, 'name') and f.name == 'CompanyUUID': + company_field = f + break + if company_field and company_field.__class__.__name__ == 'ForeignKey': + qs = qs.filter(CompanyUUID_id=self.current_company_uuid) + else: + # UUIDField(包括 Company 表自身的主键) + qs = qs.filter(CompanyUUID=self.current_company_uuid) + + # 精确字段过滤:URL 参数中与模型字段名匹配的参数用于精确过滤 + # 已知的分页/搜索参数不参与过滤 + _skip_params = {'keyword', 'PageIndex', 'PageSize', 'page', 'format'} + field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')} + for key, value in params.items(): + if key in _skip_params: + continue + if key in field_names and value: + # UUID 字段需要转换 + field_obj = None + for f in model._meta.get_fields(): + if hasattr(f, 'name') and f.name == key: + field_obj = f + break + if field_obj and field_obj.__class__.__name__ == 'ForeignKey': + # ForeignKey: 使用 _id 后缀传入原始 PK 值 + try: + uuid_bytes = uuid.UUID(str(value)).bytes + qs = qs.filter(**{key + '_id': uuid_bytes}) + except (ValueError, AttributeError): + qs = qs.filter(**{key + '_id': value}) + elif field_obj and field_obj.__class__.__name__ == 'UUIDField': + # 普通UUIDField: 直接转 bytes + try: + uuid_bytes = uuid.UUID(str(value)).bytes + qs = qs.filter(**{key: uuid_bytes}) + except (ValueError, AttributeError): + qs = qs.filter(**{key: value}) + else: + qs = qs.filter(**{key: value}) + keyword = params.get('keyword', '') if keyword: or_conditions = {} @@ -380,8 +650,32 @@ class FormService: try: total = qs.count() start = (page - 1) * page_size - records = [_pascalize_record(r, model) for r in qs[start:start + page_size].values()] - except Exception: + # 识别 entityPicker 字段,使用 select_related 预加载关联实体 + ep_fields = self._get_entity_picker_fields(model) + if ep_fields: + fk_names = [f['field_name'] for f in ep_fields] + qs = qs.select_related(*fk_names) + page_qs = qs[start:start + page_size] + # 先获取模型实例列表(用于 _enrich_entity_labels) + obj_list = list(page_qs) + # 将模型实例转为字典再 pascalize + records = [] + for obj in obj_list: + row = {} + for f in model._meta.concrete_fields: + f_name = f.name + # ForeignKey 的 attname 是 xxx_id,存储的是原始值 + if f.__class__.__name__ == 'ForeignKey': + f_name = f.attname # e.g. UserUUID_id -> 但实际 attname 是字段名_id + val = getattr(obj, f_name, None) + row[f.name] = val + records.append(_pascalize_record(row, model)) + # 为 entityPicker 字段附带关联实体的 label + if ep_fields: + records = self._enrich_entity_labels(obj_list, ep_fields, records) + except Exception as e: + import logging + logging.error('[FormService] get_table error: table=%s, error=%s', table_name, e, exc_info=True) total = 0 records = [] return {'records': records, 'total': total, 'page': page, 'PageSize': page_size} @@ -409,14 +703,45 @@ class FormService: is_fk = field.__class__.__name__ == 'ForeignKey' attr_name = field.attname if is_fk else resolved create_data[attr_name] = val + # 强公司隔离:当启用 company_scope 且当前角色有公司上下文时, + # 自动注入当前公司 UUID(若模型有 CompanyUUID 字段且未显式提供) + if self.company_scope and self.current_company_uuid: + field_names = {f.name for f in model._meta.get_fields() if hasattr(f, 'name')} + if 'CompanyUUID' in field_names: + pk_field = model._meta.pk + if pk_field and pk_field.name == 'CompanyUUID': + # CompanyUUID 是主键(如 Company 表):直接设置,跳过后续 PK 自动生成 + if 'CompanyUUID' not in create_data: + create_data['CompanyUUID'] = self.current_company_uuid + else: + # CompanyUUID 是外键:使用 _id 后缀 + has_company = any( + k == 'CompanyUUID' or k == 'CompanyUUID_id' for k in create_data + ) + if not has_company: + create_data['CompanyUUID_id'] = self.current_company_uuid pk_field_name = model._meta.pk.name if model._meta.pk else None if pk_field_name and pk_field_name not in create_data: pk_field = model._meta.pk if pk_field.__class__.__name__ == 'UUIDField': create_data[pk_field_name] = uuid.uuid4().bytes - obj = model.objects.create(**create_data) + # 特殊处理:User 表的密码需要 bcrypt 加密,不能明文存储 + if table_name == 'User' and 'UserPassword' in create_data: + raw_password = create_data.pop('UserPassword') + obj = model(**create_data) + obj.SetPassword(raw_password) + obj.save() + else: + obj = model.objects.create(**create_data) + # 新增用户时自动创建默认角色(与 register 流程保持一致) + if table_name == 'User': + self._create_default_role_for_user(obj) return {'PK': str(obj.pk) if obj.pk else None} + def _create_default_role_for_user(self, user): + """为新用户创建默认角色及用户-角色关联,委托给模块级函数。""" + create_default_role_for_user(user, admin_user=self.user) + def modify_record(self, data): table_name = data.get('Table', '') model = self.TABLE_MAP.get(table_name) @@ -433,6 +758,9 @@ class FormService: obj = model.objects.get(**{pk_field_name: pk_value}) except model.DoesNotExist: return None + # 强公司隔离:company_scope 启用时校验目标记录归属当前公司 + if self.company_scope and self.current_company_uuid and not self._check_company_owner(obj): + return None pascal_map = self._build_field_map(model) update_fields = [] for key, val in data.items(): @@ -449,6 +777,11 @@ class FormService: if isinstance(val, list) and len(val) == 1: val = val[0] val = self._convert_value_for_field(field, val) + # 特殊处理:User 表的密码需要 bcrypt 加密 + if table_name == 'User' and resolved == 'UserPassword' and val: + obj.SetPassword(val) + update_fields.append('UserPassword') + continue # ForeignKey 字段必须通过 attname(如 CompanyUUID_id)设置原始列值, # 否则 Django 的 FK 描述符会要求传入关联模型实例 is_fk = field.__class__.__name__ == 'ForeignKey' @@ -475,10 +808,33 @@ class FormService: pk_field = model._meta.pk pk_value = self._convert_value_for_field(pk_field, pk) try: - model.objects.get(**{pk_field_name: pk_value}).delete() - return True + obj = model.objects.get(**{pk_field_name: pk_value}) except model.DoesNotExist: return False + # 强公司隔离:company_scope 启用时校验目标记录归属当前公司 + if self.company_scope and self.current_company_uuid and not self._check_company_owner(obj): + return False + obj.delete() + return True + + def _check_company_owner(self, obj): + """校验对象是否归属当前公司(用于 company_scope 下的修改/删除保护)。 + 若模型无 CompanyUUID 字段则放行(不隔离)。 + CompanyUUID 可能是外键(Department/Workgroup)或主键(Company 本身)。""" + try: + field_names = {f.name for f in obj._meta.get_fields() if hasattr(f, 'name')} + if 'CompanyUUID' not in field_names: + return True + # self.current_company_uuid 已在 __init__ 中统一转为 bytes + # CompanyUUID 是主键时直接比较 pk + pk_field = obj._meta.pk + if pk_field and pk_field.name == 'CompanyUUID': + return obj.pk == self.current_company_uuid + # CompanyUUID 是外键时比较 _id + obj_company = getattr(obj, 'CompanyUUID_id', None) + return obj_company == self.current_company_uuid + except Exception: + return True def code_display(self, data): table_name = data.get('Table', '') diff --git a/gvsds/apps/gerp/migrations/0004_remove_tenant_code.py b/gvsds/apps/gerp/migrations/0004_remove_tenant_code.py index 23879aa8..28bda7dc 100644 --- a/gvsds/apps/gerp/migrations/0004_remove_tenant_code.py +++ b/gvsds/apps/gerp/migrations/0004_remove_tenant_code.py @@ -12,6 +12,6 @@ class Migration(migrations.Migration): operations = [ migrations.RemoveField( model_name='tenant', - name='TenantCode', + name='tenant_code', ), ] diff --git a/gvsds/apps/gerp/migrations/0005_rolepermission_companyuuid.py b/gvsds/apps/gerp/migrations/0005_rolepermission_companyuuid.py new file mode 100644 index 00000000..c0a9eb2a --- /dev/null +++ b/gvsds/apps/gerp/migrations/0005_rolepermission_companyuuid.py @@ -0,0 +1,36 @@ +from django.db import migrations, models +import django.db.models.deletion +import datetime +import gvsds.apps.core.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0004_remove_tenant_code'), + ] + + operations = [ + # RolePermission 表已通过 --run-syncdb 在数据库中创建(含 CompanyUUID 列), + # 此迁移仅同步迁移状态,不对数据库执行任何操作。 + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name='RolePermission', + fields=[ + ('role_perm_uuid', gvsds.apps.core.models.UUIDField(db_column='RolePermUUID', primary_key=True, serialize=False)), + ('create_time', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('role_uuid', models.ForeignKey(db_column='RoleUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.role', verbose_name='角色UUID')), + ('perm_uuid', models.ForeignKey(db_column='PermUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.permission', verbose_name='权限UUID')), + ('company_uuid', models.ForeignKey(db_column='CompanyUUID', on_delete=django.db.models.deletion.CASCADE, related_name='rolepermission_set', to='gerp.company', verbose_name='公司UUID')), + ], + options={ + 'db_table': 'RolePermission', + 'verbose_name': '角色权限关联', + 'verbose_name_plural': '角色权限关联', + }, + ), + ], + database_operations=[], + ), + ] diff --git a/gvsds/apps/gerp/migrations/0006_userrole_dept_workgroup.py b/gvsds/apps/gerp/migrations/0006_userrole_dept_workgroup.py new file mode 100644 index 00000000..20fd0cfc --- /dev/null +++ b/gvsds/apps/gerp/migrations/0006_userrole_dept_workgroup.py @@ -0,0 +1,22 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0005_rolepermission_companyuuid'), + ] + + operations = [ + migrations.AddField( + model_name='userrole', + name='DeptUUID', + field=models.ForeignKey(blank=True, db_column='DeptUUID', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='gerp.department', verbose_name='部门UUID'), + ), + migrations.AddField( + model_name='userrole', + name='WorkgroupUUID', + field=models.ForeignKey(blank=True, db_column='WorkgroupUUID', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='gerp.workgroup', verbose_name='工作组UUID'), + ), + ] diff --git a/gvsds/apps/gerp/migrations/0007_userrole_roletype.py b/gvsds/apps/gerp/migrations/0007_userrole_roletype.py new file mode 100644 index 00000000..2280da13 --- /dev/null +++ b/gvsds/apps/gerp/migrations/0007_userrole_roletype.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0006_userrole_dept_workgroup'), + ] + + operations = [ + migrations.AddField( + model_name='userrole', + name='RoleType', + field=models.SmallIntegerField(db_column='RoleType', default=0, verbose_name='角色类型'), + ), + ] diff --git a/gvsds/apps/gerp/migrations/0008_workgroup_companyuuid.py b/gvsds/apps/gerp/migrations/0008_workgroup_companyuuid.py new file mode 100644 index 00000000..e37fc457 --- /dev/null +++ b/gvsds/apps/gerp/migrations/0008_workgroup_companyuuid.py @@ -0,0 +1,17 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0007_userrole_roletype'), + ] + + operations = [ + migrations.AddField( + model_name='workgroup', + name='CompanyUUID', + field=models.ForeignKey(db_column='CompanyUUID', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='gerp.company', verbose_name='公司UUID'), + ), + ] diff --git a/gvsds/apps/gerp/migrations/__pycache__/0004_remove_tenant_code.cpython-312.pyc b/gvsds/apps/gerp/migrations/__pycache__/0004_remove_tenant_code.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6870a36375ac8fc9c6787600d3eb989e641566a7 GIT binary patch literal 614 zcmYjPJ!{-R5S`UZo6}wl{vv6@nScqX$Z?QV#su3WF@*Svq!WTgxm|<~eMPG?To;22 zm+AZ)g8ffYxw2VEl`5U!?8a4gC5!VGGw%&=X5Xyzdu4@!J_mnB(;))*Ye;vT2e8=D z;2bz`lt7AngusGB;MOT{EYQ^M8kYG2x7fGlw&(ma#6yavaa4v?oM&>f)BfZ(A3ms~V a5V|9PUfz;EB0uT%>h{&@_8n-_bpHqHDwi$* literal 0 HcmV?d00001 diff --git a/gvsds/apps/gerp/migrations/__pycache__/0005_rolepermission_companyuuid.cpython-312.pyc b/gvsds/apps/gerp/migrations/__pycache__/0005_rolepermission_companyuuid.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ea4462e9aa8a638b745bc3f442b808e84cf2b54 GIT binary patch literal 2295 zcmcIlO>7fK6rSC+*S`rS2}w)|Y9W{`Mg~=l(ne{KoA47-0wsVfEB0#RnHan4U9)Q^ zGD4!LAhoBWsuHTyNtwV}>HqaHUkVv|7nDY${hwOmus`&<#Rp zXWMxGgUGTWC@Wx;6|qnhvXY31ugoji9D)${M_=|a3yWAHf$g#@_ibHW9@xe=wAF`w z*pKD4K&QfG5eFfIaCvx}wIR||-o4dd-m}%W7QPQPvj-3(9C5E_Z+_z_iIw+l_IUGc zvmf1NU1F;b_uyWStp(uU`AYu2*TUbA2VUae^-BK1*TO%9hd1*V{h*yV{C0mU zt-aCdbWtdxwLP6Kfg%SQBg$SHI@1v~DBM>hR+SDNJ$m#V)gl#hg{U=RXhuyffZS>C zc~d9nh*c@sHk<<5Yk{p+!J*dcB}}88?-wu5&RjU5NHm0(AXKkc3>v9grHW=X)K5r* zhHPS$G`)0{&`7}|S`COw6+*+)+_(U9MYPzdpaD#lwYpxTA*fFL3n^RP)dfn1=Smon2$jc?7W19xdD!;0r ztuedCcG!fB+t*h=)&Ng~D!a~kWfu*qylhpaVO51It`7ZGX;f9O)HR*KGYi9p8<-e{ z60t3IQ_|rDQq=&P%(P+C23}yCG~zY%47)9q1 zhn@owdU0xIa&h5IdOm&f_##hck>#?;nzGov-522ijH~}qR;Onm^i;KB$pFV+@Rx+_ zI|4#CQA@0Z=ggp-(fc&M4lyBW>NXl~}Rlez3>t~i-1Zl>X6 z8h|=0<{0>rSYg98@szZI(7{=W6H5OR%Vt;~O7k4+EXO(rSO=TRcd$s2Bd)yc2A`Rh*pIz*op3k>A4xWb0x(#H(z(0PuvAhja$p21cO9T;C9 zn_53I{a3&@GPu#-yR)w)#rF2L_Mx}-Zye|kg)lSLln~rUTrnxU#D(k0 zFaEDH<^8LtI$E;cX0b=$MF+WDQcNx>rrh>Y>re;XnqV_d_8!b$1gE_7^MUm~n4k}2 l?}t}Q5(MFQH1-I+`3UWJghn>x5kY3^7zG^AjMMw_WY-hS;o0pSBSwT?j z!DDZ|coi!3t{~n8FU=`s6g;R%Z!4_`JvozYeBk2EIplxM|NYO&+fqpd?7YsOXq^=S z_{fFq(J{~j1T=_*rl5%sV5wIu1QEB%t;{<^ zV-!h9WH~CW@^8vbwpHw*+tEX}^KTu>AsOYLD@*yV?Rg=q_#auS|HxAPPg!n3#a>x9 zp;l~{Dt)wYJsSy|kJQA(r~5`mMot*Sv$2nfV+8@5moe=*zUj83$U;W8jbnQ}lD>q|d8`07&kIXiS7x_Hg zy}mS^yO}O3v1~rSc>3w}lP|C4_>GB5i7G}4WEfO63zi3A+fp z(-sa0-)yR!LuiZ#B@!3aF^^!Y>0ZNaDx;R^Ha!YagDPoJtbbIwd2z~^sEv2UVVQ_18P8vSFMwvWBxJ`JwT%q+!2JWCoCa_{$C6v(Fsp z$e?h%Bf=f>gYk|E3VR-_i82r?11q}@tZW-tDG$ycO3H(Ad2n&)<PZ)#*F`=@fa`-fo*$rg*vfb7kD$wAJgP(zH{P)>Ize})`pBK!4#fZy$if*^bV ZN8W+`>+&{1o>!Cpp}2qO8(>1}{sR>WPx}A> literal 0 HcmV?d00001 diff --git a/gvsds/apps/gerp/migrations/__pycache__/0007_userrole_roletype.cpython-312.pyc b/gvsds/apps/gerp/migrations/__pycache__/0007_userrole_roletype.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2b0b6c6b84c2b4fe0fa5d624544aad87e60d855 GIT binary patch literal 785 zcmY*Xy=xRf6o0e3x7l4Tk{p5`Nh1XDQiRw<5Q&_qK`~bt5nK+2y_?B;=YBbR8@Wy) zg`!2UNoOl!3bpkg5Nr$xIi|4?F?I(Rn^eBp%W`pwH}Ci6ef;J(^L2940bJz%Qs)%` z_^Cj#N=MFc8J%rlz(@cgaY?AT8UYPffN7h+bWS>EQ8eD)>m%RQdq%bNM}ev)ov`f- zuiK8JRGMMTc#yWnH30Sz7&3IWfrCpJxEdoqadi#2(a8xPF9D>%)S1~i@qb()rZa<; zcFcm#&P3s1N2_O5ZE3F-oRFa5bCJl|#l^)Xnx$NbIN+4=q)VT~;*l@nERj}mAg#N2 ze6N>qq2a8ojAsAs>-`s>et-P@>&?rmE^XGNtvJZSNKP=m?qxw&mN&R)#wn+f7jn#T zav^dg4Hc86^)?S!9m!cVk5eigN--}(I#iY^eVloL%2f#Y@rd!L)#j;Cx=X9Z*iF?# z7z$3WgOe|n~0 zncq6USDD|f%ztpdOn-aef3G||m@sT-pqmx**==#AXj-otC}+ou{CI&i^LysE8;w1! u((>H1=p9;DMq(|CS%mnvAyx5#P6+t{7Y@uaF}Iw(skz;$xxauQul)zEvdy3X literal 0 HcmV?d00001 diff --git a/gvsds/apps/gerp/migrations/__pycache__/0008_workgroup_companyuuid.cpython-312.pyc b/gvsds/apps/gerp/migrations/__pycache__/0008_workgroup_companyuuid.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3f6c43a9c4ab925037ec4cf309b382d2c7c92e1 GIT binary patch literal 1007 zcmZuw&rj4q6rN7Ir7erWm0%Qqpb+DRWWz~~F~-dTjS71qgk-a%ba%$3wZEKhA?2hA zCf?-6zo3CXfTNx~cyUil#)AjL!NkKR;DwVj?G{a74sYJ~&6_v#zBluAXh;KW%&*+D ze+vNoCkoE?Aw5**+PHNk~7C~yoNu_i&F10-$%Bw@i$vSkU)I%eB*~ZB5Ocztw_Gx|{lZGGSgei)w3tahhT}q;I zFD&e!YH^4vM#38g)eM8hMeM2u%^Sw6$aHutM+gVlL)dFtI3&E$R4F4g$8!*gLv79{ z*lKytaZF`on_kPO5H+Zh+>y1P4phpEmGaCi)%*bWpTtLS#Kk_T%si|wE|jZfeYW0O z4^db*gCMMPG3x&wP@U=dz&I6;DUoOwMHVUrF_BrO#PBAC7BgF*Cj)h=C&Cf=^?XkQ z>a}-TR~hRlV+WUSA6&d~P?*@h(=AMN3KJjPPwQXe&v92@?&!;1eYK;ncJ;N6zV>Zn zvj@NvScPm_ffhUl;F1lG2P9SNiE=@H^NfsVXBTDGq@?^3Y9vaf`dm3BOX)8tWx|lF z*4}0M`~Y{qgYjcIFUZ?kcX+ZhJoyJOkZk`8 D`#A&4 literal 0 HcmV?d00001 diff --git a/gvsds/apps/gerp/models.py b/gvsds/apps/gerp/models.py index f8dfcadd..5e7a3709 100644 --- a/gvsds/apps/gerp/models.py +++ b/gvsds/apps/gerp/models.py @@ -46,7 +46,7 @@ class CompanyInfo(QModel): """企业信息表——工商注册与企业详情""" CompanyInfoUUID = UUIDField(primary_key=True, verbose_name='企业信息UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='租户UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') EnterpriseName = models.CharField(max_length=256, verbose_name='企业全称') EnterpriseShortName = models.CharField(max_length=100, verbose_name='企业简称') CreditCode = models.CharField(max_length=64, unique=True, verbose_name='统一社会信用代码') @@ -135,8 +135,8 @@ class Company(QModel): """公司表——系统内组织机构""" CompanyUUID = UUIDField(primary_key=True, verbose_name='公司UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID') - ParentCompanyUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='上级公司UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') + ParentCompanyUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='ParentCompanyUUID', verbose_name='上级公司UUID') CompanyName = models.CharField(max_length=256, verbose_name='公司全称') CompanyShort = models.CharField(max_length=100, verbose_name='公司简称') CompanyStatus = models.SmallIntegerField(default=1, verbose_name='公司状态') @@ -157,8 +157,8 @@ class Department(QModel): """部门表——组织架构层级""" DeptUUID = UUIDField(primary_key=True, verbose_name='部门UUID') - CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='department_set', verbose_name='公司UUID') - ParentDeptUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name='上级部门UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='department_set', db_column='CompanyUUID', verbose_name='公司UUID') + ParentDeptUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', db_column='ParentDeptUUID', verbose_name='上级部门UUID') DeptName = models.CharField(max_length=256, verbose_name='部门名称') DeptSort = models.IntegerField(default=0, verbose_name='排序') DeptStatus = models.SmallIntegerField(default=1, verbose_name='部门状态') @@ -186,7 +186,7 @@ class Role(QModel): """ RoleUUID = UUIDField(primary_key=True, verbose_name='角色UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') RoleName = models.CharField(max_length=128, verbose_name='角色名称') RoleType = models.SmallIntegerField(default=2, verbose_name='角色类型') RoleStatus = models.SmallIntegerField(default=1, verbose_name='角色状态') @@ -218,18 +218,44 @@ class Role(QModel): return User.objects.filter(userrole__RoleUUID=self.RoleUUID) +class RoleAdmin(QModel): + """管理员角色表——标记哪些角色具有管理员权限 + + 只要在本表中存在对应 RoleUUID 记录,该角色即为管理员角色。 + 不存在则非管理员。通过单独表实现,避免在 Role 表上增加冗余字段。 + """ + + RoleAdminUUID = UUIDField(primary_key=True, verbose_name='主键UUID') + RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='roleadmin_set', db_column='RoleUUID', verbose_name='角色UUID') + CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') + + class Meta: + db_table = 'RoleAdmin' + app_label = 'gerp' + verbose_name = '管理员角色' + verbose_name_plural = '管理员角色' + + def __str__(self): + return f'RoleAdmin({self.RoleUUID})' + + class UserRole(QModel): """用户角色关联表——用户与角色的多对多映射 通过 CompanyUUID 标识该用户-角色关联所属的公司, 用于菜单按公司差异化显示、数据权限按公司过滤等场景。 同一用户在不同公司下可绑定不同角色。 + DeptUUID、WorkgroupUUID 为可选字段,用于将关联进一步限定到部门或工作组。 + RoleType 标识该用户在部门/工作组中的角色类型:0=普通成员/部门员工,1=组长/部门负责人。 """ UserRoleUUID = UUIDField(primary_key=True, verbose_name='关联UUID') - UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userrole_set', db_index=True, verbose_name='用户UUID') - RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='userrole_set', verbose_name='角色UUID') - CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='userrole_set', verbose_name='公司UUID') + UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userrole_set', db_column='UserUUID', db_index=True, verbose_name='用户UUID') + RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='userrole_set', db_column='RoleUUID', verbose_name='角色UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='userrole_set', db_column='CompanyUUID', verbose_name='公司UUID') + DeptUUID = models.ForeignKey('Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='DeptUUID', verbose_name='部门UUID') + WorkgroupUUID = models.ForeignKey('Workgroup', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='WorkgroupUUID', verbose_name='工作组UUID') + RoleType = models.SmallIntegerField(default=0, db_column='RoleType', verbose_name='角色类型') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: @@ -244,11 +270,13 @@ class RolePermission(QModel): 一个角色可以拥有多个权限,一个权限也可以被多个角色引用。 角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。 + 同一角色在不同公司下可拥有不同权限集合,通过 CompanyUUID 区分。 """ RolePermUUID = UUIDField(primary_key=True, verbose_name='关联UUID') - RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='rolepermission_set', verbose_name='角色UUID') - PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='rolepermission_set', verbose_name='权限UUID') + RoleUUID = models.ForeignKey('Role', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='RoleUUID', verbose_name='角色UUID') + PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='PermUUID', verbose_name='权限UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='rolepermission_set', db_column='CompanyUUID', verbose_name='公司UUID') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: @@ -267,7 +295,7 @@ class Permission(QModel): """ PermUUID = UUIDField(primary_key=True, verbose_name='权限UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') PermName = models.CharField(max_length=128, verbose_name='权限名称') PermCode = models.CharField(max_length=128, db_index=True, verbose_name='权限键') PermCategory = models.CharField(max_length=64, default='', verbose_name='权限分类') @@ -293,7 +321,7 @@ class FileAsset(QModel): """文件资产表——文件存储与引用管理""" AssetUUID = UUIDField(primary_key=True, verbose_name='资产UUID') - CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='fileasset_set', verbose_name='公司UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='fileasset_set', db_column='CompanyUUID', verbose_name='公司UUID') FileName = models.CharField(max_length=256, verbose_name='文件名') FileSHA1 = models.CharField(max_length=40, unique=True, verbose_name='文件SHA1') FileSize = models.IntegerField(verbose_name='文件大小') @@ -320,8 +348,8 @@ class WorkOrder(QModel): Category = models.CharField(max_length=64, default='other', verbose_name='分类') Priority = models.SmallIntegerField(default=0, verbose_name='优先级') Status = models.SmallIntegerField(default=0, verbose_name='状态') - CreatorUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, verbose_name='创建人UUID') - AssigneeUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='指派人UUID') + CreatorUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, db_column='CreatorUUID', verbose_name='创建人UUID') + AssigneeUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='AssigneeUUID', verbose_name='指派人UUID') Resolution = models.CharField(max_length=4096, default='', verbose_name='解决方案') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') UpdateTime = models.DateTimeField(null=True, blank=True, verbose_name='更新时间') @@ -339,8 +367,8 @@ class Message(QModel): """消息表——系统通知与站内信""" MessageUUID = UUIDField(primary_key=True, verbose_name='消息UUID') - SenderUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', verbose_name='发送者UUID') - ReceiverUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, verbose_name='接收者UUID') + SenderUUID = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='+', db_column='SenderUUID', verbose_name='发送者UUID') + ReceiverUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_index=True, db_column='ReceiverUUID', verbose_name='接收者UUID') Title = models.CharField(max_length=256, verbose_name='标题') Content = models.TextField(default='', verbose_name='内容') MsgType = models.SmallIntegerField(default=0, verbose_name='消息类型') @@ -361,8 +389,8 @@ class Menu(QModel): """菜单表——系统导航与功能入口""" MenuUUID = UUIDField(primary_key=True, verbose_name='菜单UUID') - CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='menu_set', verbose_name='公司UUID') - ParentMenuUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', verbose_name='上级菜单UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.SET_NULL, null=True, blank=True, related_name='menu_set', db_column='CompanyUUID', verbose_name='公司UUID') + ParentMenuUUID = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children', db_column='ParentMenuUUID', verbose_name='上级菜单UUID') MenuName = models.CharField(max_length=128, verbose_name='菜单名称') MenuTitle = models.CharField(max_length=128, verbose_name='菜单标题') MenuIcon = models.CharField(max_length=128, default='', verbose_name='菜单图标') @@ -425,9 +453,10 @@ class DepartmentSMTM(QModel): class Workgroup(QModel): - """工作组表——跨部门协作单元""" + """工作组表——跨部门协作单元,归属特定公司""" WorkgroupUUID = UUIDField(primary_key=True, verbose_name='工作组UUID') + CompanyUUID = models.ForeignKey('Company', on_delete=models.CASCADE, related_name='+', db_column='CompanyUUID', verbose_name='公司UUID') WorkgroupName = models.CharField(max_length=256, verbose_name='工作组名称') WorkgroupEnglishName = models.CharField(max_length=256, verbose_name='工作组英文名') WorkgroupIntroduction = models.CharField(max_length=1024, verbose_name='工作组介绍') @@ -475,8 +504,8 @@ class Backpack(QModel): """背包表——用户物品栏""" BackpackItemUUID = UUIDField(primary_key=True, verbose_name='背包物品UUID') - UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', verbose_name='用户UUID') - ItemTemplateUUID = models.ForeignKey('ItemReference', on_delete=models.CASCADE, related_name='+', verbose_name='物品模板UUID') + UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='+', db_column='UserUUID', verbose_name='用户UUID') + ItemTemplateUUID = models.ForeignKey('ItemReference', on_delete=models.CASCADE, related_name='+', db_column='ItemTemplateUUID', verbose_name='物品模板UUID') _ItemName = models.CharField(max_length=128, null=True, blank=True, verbose_name='物品名称缓存') _ItemIcon = models.CharField(max_length=512, null=True, blank=True, verbose_name='物品图标缓存') _ItemIconColor = models.CharField(max_length=6, null=True, blank=True, verbose_name='图标颜色缓存') @@ -675,7 +704,7 @@ class PersonalPage(QModel): class UserSettings(QModel): """用户设置表——存储每位用户的个性化系统设置""" - UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, unique=True, verbose_name='用户UUID') + UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, unique=True, db_column='UserUUID', verbose_name='用户UUID') SettingsData = models.TextField(default='', verbose_name='设置数据') class Meta: @@ -696,7 +725,7 @@ class PositionTemplate(QModel): """ PositionTemplateUUID = UUIDField(primary_key=True, verbose_name='岗位模板UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') TemplateName = models.CharField(max_length=128, verbose_name='模板名称') TemplateCode = models.CharField(max_length=64, default='', verbose_name='模板编码') TemplateStatus = models.SmallIntegerField(default=1, verbose_name='模板状态') @@ -736,6 +765,7 @@ class PositionTemplate(QModel): RolePermUUID=uuid.uuid4().bytes, RoleUUID=role, PermUUID=tp.PermUUID, + CompanyUUID_id=tenant_uuid, # 岗位模板复制时默认使用租户UUID作为公司 ) return role @@ -748,8 +778,8 @@ class PositionTemplatePermission(QModel): """ PTPermUUID = UUIDField(primary_key=True, verbose_name='关联UUID') - PositionTemplateUUID = models.ForeignKey('PositionTemplate', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', verbose_name='岗位模板UUID') - PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', verbose_name='权限UUID') + PositionTemplateUUID = models.ForeignKey('PositionTemplate', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') + PermUUID = models.ForeignKey('Permission', on_delete=models.CASCADE, related_name='positiontemplatepermission_set', db_column='PermUUID', verbose_name='权限UUID') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: @@ -768,7 +798,7 @@ class Position(QModel): """ PositionUUID = UUIDField(primary_key=True, verbose_name='职务UUID') - TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', verbose_name='租户UUID') + TenantUUID = models.ForeignKey('Tenant', on_delete=models.CASCADE, related_name='+', db_column='TenantUUID', verbose_name='租户UUID') PositionName = models.CharField(max_length=128, verbose_name='职务名称') PositionCode = models.CharField(max_length=64, default='', verbose_name='职务编码') PositionLevel = models.SmallIntegerField(default=0, verbose_name='职务级别') @@ -797,9 +827,9 @@ class UserDept(QModel): """ UserDeptUUID = UUIDField(primary_key=True, verbose_name='关联UUID') - UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userdept_set', db_index=True, verbose_name='用户UUID') - DeptUUID = models.ForeignKey('Department', on_delete=models.CASCADE, related_name='userdept_set', verbose_name='部门UUID') - PositionUUID = models.ForeignKey('Position', on_delete=models.SET_NULL, null=True, blank=True, related_name='userdept_set', verbose_name='职务标签UUID') + UserUUID = models.ForeignKey(User, on_delete=models.CASCADE, related_name='userdept_set', db_index=True, db_column='UserUUID', verbose_name='用户UUID') + DeptUUID = models.ForeignKey('Department', on_delete=models.CASCADE, related_name='userdept_set', db_column='DeptUUID', verbose_name='部门UUID') + PositionUUID = models.ForeignKey('Position', on_delete=models.SET_NULL, null=True, blank=True, related_name='userdept_set', db_column='PositionUUID', verbose_name='职务标签UUID') IsMainDept = models.SmallIntegerField(default=1, verbose_name='是否主部门') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') diff --git a/gvsds/apps/gerp/views/__init__.py b/gvsds/apps/gerp/views/__init__.py index e32c14ca..ad7a15f3 100644 --- a/gvsds/apps/gerp/views/__init__.py +++ b/gvsds/apps/gerp/views/__init__.py @@ -38,6 +38,7 @@ from .viewsets import ( PositionTemplatePermissionViewSet, PositionViewSet, ShopViewSet, + WorkgroupViewSet, ) from .twofa import ( TwoFASetupView, @@ -81,7 +82,7 @@ from .asset import ( ) from .message import MessageCompatView from .menu import MenuListCompatView -from .form_api import FormAPICompatView, FormAPIView +from .form_api import FormAPICompatView, FormAPIView, CheckAdminView from .doc import DocView, SerieView from .position import ( PositionListView, diff --git a/gvsds/apps/gerp/views/__pycache__/__init__.cpython-312.pyc b/gvsds/apps/gerp/views/__pycache__/__init__.cpython-312.pyc index 1405248c00fbafab6701544358a4203f08f23565..350ce6d0652b919399f2a5955e62e4e2ac1e8481 100644 GIT binary patch delta 736 zcmaKoO-~b16o%&lmC|-9;z#^KO=CChpxMzmkX=6-G zaOIxNPNV<8nAnxSz=dp#yY5VMX^bnk&bdFpN#@CWp7$ho=KdIdb4~rCsv*UHUVljM zYscy(cJ+AlTMA{iz)6_IDVUNz!o67* zKf(kgO$DXSw&C#nZS0JGcwG(w*Z^u?kh( zgFVzh!y442KhJeEz>xU@H?a`5?Ybll!|j9u;w1%9$ncW~Pg2Myu0JHJo8 z?_s^(+kqRAZZz98^@Ak?ExYq+;DoWJ_bU7=BWC>D?FL%UoVq6N_*u*n?h@`%)vxKg z)3#e8L28bWB%}x-?_=clky!C_cfhq;X0`2_LgcAiB@_s2gd$;`u;HT{&|KGSiA@qE z!WN-SsQBpiqv^U@!xZ;v>O<3Riw6{gbw^-TJG8_@T7IM`*A?-YnqI@v#Wu+&a`Djd zzsPcI@1;6b5;ZDxD#GYKxAm$bGLG>Z$1w)VY0 l>Tu|VADREUpS_FGnb;N9^H=#V5}qB+_2s@QIj<9){s%4E#|Qub delta 724 zcmZ|L%WD%+6bInk#F(UM(nedMMXS)}(TCE~wC2^srirz-O`7^X+9s2kNsZi-DR&Y? zUG#4#vlLti?u9JGy$ivmyI>ZAA_#&DMR4VuyLVtd&iS1Kcji8y_;N1rArKgF*iS4t z|0sPN7-J*H(T{27(7f6JpTsGc!Z3s}0uhWtROWsj!)cht8JNLYn8i7mlldUOh?l@x zV&Nm%2Y3RLki-w$277j(TpzH1)RFqP=aRM3jjgo1O=C(^L+0QK9Yx>aC90rPA9l1Xh*9rno`N zHz{*zhAM7Rdt0s?8upP*qb2T8c9*zE0CC^OdQ0EnNApQbttuKeTcS=gmCy*?rsr)g zBwF&U2DN_rhpMKxl_nPlwDph}B|^lI&0lW@4XwlVtaxg-%zn*xHa}gAvA!eHcf|UR g#a}0#RxNmba-2E;IR3+LIasz{1`8~1eGe}D19{KCVE_OC diff --git a/gvsds/apps/gerp/views/__pycache__/form_api.cpython-312.pyc b/gvsds/apps/gerp/views/__pycache__/form_api.cpython-312.pyc index 4e1f8f1bdd1cad94d5846bcbdedcff81b0915afa..e59ce4b2ea9dfa2260c12a13da1074e7fbd3691f 100644 GIT binary patch delta 4466 zcmb7HeNa@_6@PE{?Y>=hSw0qcAS+*jxIi$eG$avq@PiLQ8bhK9A=#|^p124sx$mK3 zmoC$4gPAx%ZyKpX8k{B#XwsP2CN!f{`%fpG{Ij}FU0-n~?FgEg&WJIY&g8G2a~~{A z#!07lX3ySx?z#8f`@6q$?>V3BJQ8qyk(!#qL3!iDx^B-yCtSIF_(b`STV?SG*I_ep zjht*d!pU~Ue9nw!9M@rI+7!?_lC&vI>jZ6TlGZWk@}`}5ZP#WO&vBz z8#H?z15d(gW=Gzcd-H?aZ%odOd^$Jw;q2(|ZcTr7`}b#VkN$G@#Ms@7r|*1n^!8`( zEf|cvIeUrD9y@;Ly_0v2eLOopS@cw>N2!;4gMCGJ&P?8#{y-N7RE4xP=#FN!eV|_< z*nC=d1_lUG`ZON_`}G9j^76VpFRk+Rkr{xgQ^Hy&<}9+G=L9^2b^yE6&TYto0-I8;-2F+jHGr zdc|FO-CcRbT{&%!x+`Pu&DY({%~#ybF?UPU*%Ei8--jh|fn+u!XvqEhB|pUB0^~5# zhPTk-OgD{+_6pg&e+{FtfE-jSmeJpmpg#1|iY zfnY$nd-1h9ubiEIb@JB5iP@v?8Z4eY`u^cp%gp>G$^yGB$b5q!4ArW-_JL`lQh@%(k6->>b{tpqN- zU_U9Qen-_d#8I~c*L<3PPmjX5M;>C3&%4zn#Sn(zlkIdq+mp=zT#ZHu{8xX&S~)GwY2ZugzMN{lo}S7HbyT9d z0Yw!G#N}J`@44}_x8Htujvl{tX`Hlz!khUZ#0_}ba40gGAlRfGMFD+D%I5db15(Mp zov6Uam(A~WJ_vF0H1->4LroWhn$ipJA=!m`c1=8kwMRgxxI*xhL}6C~dF*6vzO@tk z^J#JJvNLHk&f|3MbHeUe{tkLNw}j^9Eo)@Zsl2A-UQ{^fx)m=9wXyt`3%^=1#lsnH z@>H&^!-Z@@fw!-*^T(hz1x|ztMwA+?p;$+M?J2jijvDaW?KFL5%c_J=PMMa_Q1KnZ z3jl}K(Vwl%@q7x^`xzjHIe0l=A3QcVCdJZG}oEK7iZJ| z6x5e2J$~@;nZo9FF6mi=II%kg;8~7qLq!C3Eie?aA z(QszQJY0l9KfDQy7vw24vM%9E9>y+4D8@D{J4mgT+njGBgWJ#!A=> zFFKz93gcOL;Q^?mxEX-5Ay5|zOa5Yt_+cD*4#jp93^)c&2R5HafgWH0_X5^@DE6Sh z^CT~!U;sbB^^G$Kpb7K&RKxKYz|6HHOW?|A#ZXtU#=fu@YdcXadApc||0CG-phLj@ zvLjeL1azmv>B-#d52@N07$d14^UNLS^D7=*iLN=)dLu1AF0G2DuUdW@7dEbbfiFeR zYa{zWk{%RGF4~7p6n~_DE?*sC9oR$w5(PfS%KaSpJH;tn2QPCSCYkRr%O=RW1V}9e z-L++(64=iY1N5{3gk$cv!*g#hgkH%B$L!Hh=f;mEa*o@_{xtX2FYaD!8Tmg`kGmI- zJvj2M;o(^hL`+FEG9BVcDx^|N0jfzhEhS`&In$6iISfr`g`{zq$Z-kET9)-#l6tHW z**YYKEhHn!!~%Jpb#d?^UN!L$Cl85Qs(~>F>1!C`}i+s6etW!$v55R7=+ z4By6$G#KF_i#X8}jHIf~->YE472)7qY(dq>f?3@OHFYsT`(A9?2Sao>q+PznkQJg@ z3~Jfa2=|N&W;ecz!Gd-?3m|lb_esKHi*}QxV>%^ z`$6cU&nJfhKA&-Ef6w0zp&5#Q!%sy_j&L`Gp8G?T+9wKbK`_`1l& hTfZ)$$JZ3(eZ$r8P5d`~d=~GBW>wR6U>gFHF&K!EuJpZig?6p?y)ipW zMhFS`6*g}Ss2E6$gb5^?89)9|B$|jZ(S@2g9$=IZ4F3@biGhDS=XD>QS)w=TZ|9zK z?z#8=&N=t(NaXWw(@#dDj)CD`@7s5&cHFdt?HjLK-lSkU7*2D9;R<=>c_lh9%8 z6*>tl9LVv=89g9yLLi(<34th2Qh@_8epm?fCrKg{PR0DGw4@DTFfI`rBqms+hM({R zz#%{Lr`|j!{$}%CwfUya)*E`uSny=|nGIQe!&QCvh3F^Tms_qZ&-%NkRNW7l$;ax$ zY=NYNgV4i;yI2#wUf5$YU^pbg;cyTmZPu2TIbdc)brRqZb6Zn1W<4@WlY zdLP37Sa3z}_Ba+Sh$vt>JCGke{2ufv=`WU~QhA=8JZf;Wta9?AaR>aN7mZ?px3mi? zA!DvxYXM(L#f8Q2h&Ircj1MISg}_Kbpo13!WCs}}>KkVcJyKs)B?*%*>OXW!}YjM$Kl^*(HF|Vr4+B@j?#TQgu z*49Qv%UV0;WO0grr9WFz$S^%>h|seoYk=KQX$SRL_sG&tddr%TrR>c7^|D)#h=gs8 z(k)Z-B%bssgdl*Vh;cxD3&#Z{Wtij0Dmt~aqEx241f7ucyFa32rL4W3EvB7zKifoy z?aAHcs9uh+6ak5QhY)Ke|KTy8ZPj(hLf(g{(%8g}KS`w-ybUb>cwSfUttD zt0-l+)82}jfmT$sAYgp2VX>5ucC_cx0g|MNiF_gy<47m!<+ABOsTx4QLqO}z4I<~x zL;t9-t8p`-oU)bm>bWF!Dm!_^Zk zG_cdz7-+=p{RmA6@_juZP2?IdcZM1?A(Ycsopoxg0#XD0|Bh>}mnRhS_ZJl4fNu!; zHPXJCvWgEte1N@ih?zE*98bTNK55IEol}KQVy0$SYsFk3vKPD-MDWvlHCFu@P?HG4 ze=caE371X(Vcvq{u5z^ln(_og`|2C%HCGEAH&`oXpZSBzjFKoa${t3<_d*`@xS@|0;*I%l{Tk=Z{~~-{l;22dEIkaG1>zrvI$`of=r)G&}N)&LFhujH_8W}iH*5Vl4*9Jc66TNUYyI^??g%7VHZlR2=nc$$;$$EZ%`_FE?lHTaRtNX0}h;TvJ7^{A5i2-S84y zi~ZC^b_0?<2y%bP&C`cdg!gIM=kdt~oOXG2l8wM8H`EWG27Wk6_WNO)N)Ey&K)!8| ze%IvNivK0#imnDFT>7sq9u+nxlH5?7UqkkT6?%jbFlYCaEX&?xd^Z{QO(u9>tzx&a T_f2%mvMT*UCdhWO@+toTesCPN diff --git a/gvsds/apps/gerp/views/__pycache__/user_detail.cpython-312.pyc b/gvsds/apps/gerp/views/__pycache__/user_detail.cpython-312.pyc index a55541f3a1a204ca3f59f520f8539128acfab808..bd990c5c7039c7952f6db625434fc01b06700862 100644 GIT binary patch delta 1793 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 T8VyMV5xegX`aeUJ_OyQigc6hH diff --git a/gvsds/apps/gerp/views/__pycache__/viewsets.cpython-312.pyc b/gvsds/apps/gerp/views/__pycache__/viewsets.cpython-312.pyc index f5d7dd2a0a7c784fe494177582919c408faf3aea..cbd4efbe340504b5c185603f9d099919f18d2d06 100644 GIT binary patch delta 11956 zcmb7K33!y%xt@PAYciS1!ep|LjS#|wB?}Qn1qlHJ5eSQH0h1v!gn=Xz&kRdwaNrS? zwSWg?t$}FeAOMo`CR(e~=S7Zk5|>GSk{@A+rT&jjr}2MgMFO8m_#yH5^u+>iCw!L2 z8l{{}GTD|0ii0TGQ9Nc?D)2dk&yC0P-)jx#Ji-o*$J!bMcRt|@2H=+qd?DeB2H;l+ zd@6C@w*95~X|~#kNvVMiFIn{LHpy zfgeNou>XHBUhOtot4Rf2aMQEH;RB+K*`fxnCJ;|KOgUo9wi6XhNi z#VaTih;r{hifxUc+((rA<7ct075IsSpELm9D)0{we)0f(o50r+eo8!E-`=Tp)`j}@ z^^%kmavCe86xqDk$JVwm-@1;_Qq#LS$<0TmHb@nrZK-ccQUkBE)nu(Ei$1Goi|9Yu z$UKj&^Y{a-iSM-)NQ?OeTd`Ebd+fvc7q$h`GCn=6(!Pc`*;=rhh?#ZzJpne7?@oKT zbO|VW07l@fhAl&TF~IByaPq&WEj9?kY`!%8aL^A*0AL+J8UUt{t$t6dr#aBbJgu&P z$IaG*;sn?LkOHs~zzARh=mgjV5SzIRj{uYC&tFYoo2K8E+h-frYa7;Qk0%<7#dQWE!XfEgTDs!Y4dC|!( zK61jmtQO5?eOHoO<2L4MrfOD@kT)B;^nBh!_EN)|4CvC-S?YY(`*?S9rwoX#XUB}{+h zk(pDY4|7~?9@!G#ldS@~jd?MN5K1~)F{?>)uW_}v_{O?dj&1X~J*|GWO%o)JR$`M4 zu8s~*yPG|QK8Ut*5}700fybQy1_Dl_FjwXSq6we_U>g9;S(cjpFp47Or)Ui(xyc@0 z=_0RZ!varWtl#5e&8zD*I@0hPyxm*KKelHEKd_Ydr)M6{Igs;ST6(wRT3QwUFT0jj zeKSc)ANOTaQigTMlp7{ZTE-3?aqh3%KYzdD3yt1v+fmzZN!vGT=dAvTlYX+|(uxxs zPCa&J^Xbi33-9VHoN%RZ!sYt;{e?v*Hy_>HUsQa?dfIx^sLQc_rO~C@ZY0qFw=9}q zTK1{*?y0@mRab4*mrd2ez~(s9n95ZK%2n}?a>yW&e+btY=%8v(*@C_Vw3=zWJ8Pyr zO;20Z7)IUHQcLluEsBp6I9|yYDw3`BF6L8w0_{f_RO3l_v!6pm}gCh&e z1RE?yZCwkQ8*DI1#eB?h<^wm0nU63N)&?9*LFk*cQ_GLQBNQH|?`;0voNVnH4gd48 z68=ukBhs=^aqjoEQVajZ(7Dnx+@3$8vIWB&jf~|?RfZIniy7o(ub;di;A(I7uswv} zYxBnr7ZECLU5A@OQ=^YHHZu>k*aV_G&0*{0Z{#;n49zL1mHeTGf*MI3>tZ{59Mg*c zHwvvm6&MY!>;m{yp>7{=0Fb61GD4`!>9ZC0+KT&ZmE2m?q{O;?LXV-nMOx{Of$m&! zQPy6JdV8RYXxPvMx{FICc|k;qJL)nMywOnb&*?H_zOU6Y&~cxp;rvkk7R`qlIeq+H zdluj8&E@ZuOb8+>CvY0o3WUr@V^xF7{LW7E68ZulV)^F~fXp-Z@h67QRAl{aYPbIcB1Tq1jApM;NW}rqy#CS%%*(ei1c;)$0RE$J^FcBb%j;Z1!KVtx{0KJ~nn ztVta5Q&k~<9#cs}m~RSp9;<{zq>lv(Rg4IM8Q=tVR>#k+%;m$13hgP;U{5V2*oTy0 z|Jm^N;4{Q5o8W-Z+#IJ>wJeL10wvKnn6l)jLv92;|a{hmh8#%9Vu z#LbiuxW_pmxZ}hs^ulTf1&)W&aF3VgQ?fWwULw5_>M55b zs$NuZPPO>9}_}f8=NH%I+ z-yZtw)`K6vw)grEj$A*{E%r)Dieg7Nef|!ucL7j*Dryv)GEjNu33#$^_p7U-Ge0&a zj~4yvm=fsbWGGl0sH-vAB(p6^F_w~b#d9l{M2 zk1DqE2HDDwtL?DbdJk*!`u(K&uz$1HiC;DW56cn7%ybquAJp(x{?=w6zc*0Cs}5B2 z;|<9cw;_l6bm=Y_&nNM&$8-6IWoEm+OV5S}GKnC5Zj;*_u=9fIvV!CjNp91Hl=ILK z{T65f@Kf=lpvt#2k)- zE33p5w)iQWX>(W)bq(7JQj*vx`-oc3B;HY-8=#UfB(!W=PeS8r_iOuT7eiw4J59%Wjx8=JcD% z{I}!gTl(|LZkhGg`@7G72}T5(g#?(H>g{GPzDh`Hn{+9YEM8vyb&%b*-=BZX($ zbQesKlNnKPBpd>J2-3w1oe?)1y9{pg+NmTMYn*kN-O28h_!JyRv$#`1b6YN0BU93m z&vqw+(6Xprosy#2pipf;iBAL(_8wX?03xI_naxD|TWI=y@Ilc|Q(QWZ>XUs6x;VD| z39SzZ_)Qv3S~xcsh0of5CUxdnn5WIR-t%|xT!d1aR7y?F>q{-^O)cq5E$>Y&k18{) z+#7n0b<~%dBq;5sNn^f2gGO_wRniEL?Os&4>y! zbpD>PQf3o9#V!cg6Ci#*p(4JtUqv`)O56#}fj+{?*P^uxU>g9Ei0Z5spZe5i_s57m zaebDZds_q#pWTN@{H=R;DgmE|>hI(F(jFQ{iGWIgucKQAwwaL1qbN3A+*Dg_zX4?G zLQlJ^T^wIUIOKC{^QI$%Dkg6f!IS+L5a4Gw0lonE65tj9yhfzq>pPBLfBjN84WHx} zYbQ@_0mD&%^8{oAWqTj94P?NBP;m{?nR%@pU%dhlNc=miiFvyxhJ z+Khz@>l>>ps~YR(Ppe%xZ7L1Z(drGb-JmA5dfMsoU=jNG03PFKr!3BAIM>PgW)hBH ztc?9Ph{|etUR@3)_Pgq;v;|UVb=|XC_1QJHIHB|P=!?aCo0N`R&5Tr@s8jl9AOKj2 z=!Zo`53qdu^p%R1KjZ&CeV6o9Xyb#kj0P8aG=+XWCr>*ug*wT`HgEevFI`F*+guyb zP%ad5a)>`*=#E>o>yS_J&@Ua+PsRQQ`D5rwSKzY6dCX|NP7U10e16) z^PC1nru=8~CS^AP+d)81@iDTu##LTVtD6nyIrAToO8ClqB2F&W?p{5rvji$&NA}*Mz z@I^r@R`CpINCFI{Q(PU;hDVrw_~uzL%ol-$44H@oh?wG}#uoE)OUtDn^FJ&NhCN-B z`2J;INX8!GB5&ud%SQxV)Rs+kbQ9C!W1Y-FTO^-nULW%YI>jQLM%g?U`VmU4Ck8eT zU_Jo7w$ZSM2*@_ljkl%UBaS8$#Fo@W= z5L#^G^J{OA;F-9c9avIsGiwnfU-huV& zh!dtxQaj)oD}By;JZ<4PU{fEaVxz|50Q~qHw%hMFr0S6-FA=5 z=Xo94|D!Z-FQLmldJ7f$$h%ZZB^5<3mf2JM!L=iTsz{U^7wrd~+X)Fo7eFSz2*Zjr zDJDo5jfh3IAz(MI>}(95OE%^4&)4Q@by{w2-5FNhZ8!F+Wvs_scLQtyNEE__<%)c{ z4l7j|tq(YWujtQ@{X>7^@^J)ze`N_@)|REH(8uc!=kR@PZB2em6#!TVFccuB6|tmy zfJ?L_69^);s1~vT3js*e^5yOKD06P0IXif=FSkK8XMTb?Ck!@cqUky@9e_j2gTC>~ zbe+OVulcSSIK~pM;zDnCJQ_<8quIouIO9fRxDY7aX5EJ#aj^^I7erT?D00Wbys(~o ziFttaT#bw<>%0L^Tg0k$!pA>nqqW1eJlFpWS@SFYncB=AVh@Mq%{2L0iEZYo>+^#m z(Y}YVjsryN%#VNa(kBQ1B~oV+$(hOS2gkro$VnZ(4t5HJSnYBekFnh~(4%ab>ht*L z_yW^5l3BCoz=3^Dll5Zt>`_QW1aq;|$SV%;#dc~2uQ;I7OA|C#b_S@p+`|83JzgfK zZm5=5LsF5`51;4dwzodqchjN4Lu+TI)jQm zV%du4ih@kFsrNyB3VpDb0h-wf>@I*U1R{zNdfB|GTAIu+Zu+IV+KovomU1A7gkB;n zRygif%(53?AHXK`SO%~d;5)zp6p&i|c)JPj9olCb-pjxG&Q`^$zay;rP}i-3XNW;g zgOnC}+B)z~9MXzhT7}MRZqugs5Kq{Qp2p};@X{?MDRD-`7i<|G{5f$kLB}1TQoPZ9 ze_Vk>)-(xB1FcCZeNTfB6Nu15RMLt@3c;fCnwY~lCK5TpwE$1uI%d4kiHR5}8;YMI ztF5%WP1#di&1*a8FH$tS-0nsg+QLxB*1Dv4Cptv@aw4`pvR|q%9>8){ zTYVDrm}Q9_i7=UHzdOJja}lw>n2VSqKEgY;=N0S*?;ZkvbKEY&*ByKL+3iJAOXxS- z*Cttdh#pqvAzr&P+kOmPj|0SOw0&p3bdc}aIVLE|k)za0HqZC8(Yt$ZdkbC8O(u+b zdtw)fwx`mhFc9HPaQ*~J!`LTiNpgz2#OrT%F?S=KH`({F%4Z4SQb(g`Qn*;dt_o>G zX!ou$5|`Y${L1ck=fWIss~k)$W-u|~i#keGG(3!1;BI0Ts-XUJPnoovJN7OQr{N5B z1r;0baV%ftJm~COd-LzWQkIdm64l}=R|BMu&?{HqkYlSoW@6j52Xd*H& z{>jrzr1|^-K2WS7>1MYjGsUq%_hcu(uhwyZI zLBhu!93`C!H6FA}t%#O$$c9y7Q_U+hI0b7-RJFw7@efH(YL%S>4&WI7@X$iK&Yg95 z9Y6O>x>{4i25wbMIrWZHsx*az1q1jYw;#Ssv5Qy9F1D*@DoHTYw0~x%*Dw>n>%8K~ z-O5a_i9E9X$f3b%_}XKIkA7nfKYZZQV)~Aj(1U-AQNX364paEr?pOSSW1WMQ$*Ng| zsHYDm0K^xiCXLm~D#>4hJOF+QuZ$dX3j6pUj!##lbBUz$a_}P>D#5$W(&(mR;o`w> zUXE%JZ(@mCey{s-0y`iUfvYS!k1`6o?VrX^53adKCBaB zc!Us<57PMIXCILM!uy^rvA|a%Uk%_pv{2zmcl3z1^HiY$>ZO;QFP(A(S7FTfJKPbV zsXQ6?N3vnPtChY9jH@3F=o|M2lQp5QI4Oznu!XuMzQ)apyigqy$>7Rc_KzWw=fzw0 z4W|b^J*k#>XR{Da|1-#ZP_uXoRDky|J<1hmgx)p>pPQwa#aluTe|zp!#AzVa?(err zDZfEqv2PbIdLd70=J&sltw_}(TU*xq-G2Al$oFmUfH`*cm_W+xZLsWt9FqZrh~vI; zlg;#1UdL>FE|*Z+M(R{lWD#*R{P`3A%L@+afB4rgTnxL}ZTBdbF{fBtFIur>eGADB zD%4+ND1a{@)Q4$Z+xUnx59h%+1>WhCjg5_NUvp!lavRpj_nzsKT-?*WBSU=OAg9m; zqSw{x-Q;0@{*UgLgW_GaY!Um9yuh7}0lNUU0PF*J2_OV;3E&NY4*@;{Fk@-B;t^LK z;@m9Gtl}i8RO2Fli zCZT^C{|>(5Tt$gd(tfQO1bih3Y5e7Li!z2vjrl|7XZ60$)8AwUrz0RjpN0f`6*0c9s@2&p0|l3H?K1xygD5p0Qy z1aGs5f(U8@0u7Xo&bZ;y+O3WnwLALMZoeLJ>F=9SzGh~e>Cv9^pO@6Dda0n@`N+?E z=Pu{mv)yy=%c-v{AAe*?e={vDIR-yz6&t3s?@rHEPVOH4Wr`BxNOh#QOmLU4k$K9_ z3?;@Ab6t$Xwmrs?;f&kxxNIGfC5vJcZOfRRN%TPx^yN&?BKqKPy8N4BP01!|jx#+( zUt~$Nt>pegs3kW<$Na09o=5cjaJmdwtVsn#EexmH)^P8kL?0GGuVs1>(Tl_BnKvR+x79PfjOgPc=6r~oFDH71iGzb%CQ{3!@DVZ)XHA(*)XK>Ind`a#6l$4jYH8$_E2!nl$QD}@ zw@jm!>ES$VE~Z~a^s6K28<;+W=rbee%}lQ%`mAtzd`r8V=+;L3ywC3t`0 z+i^;@tWT>|CIp{PdqGjw$lGj{IgRA7$K%Cv`cF#`&ek@k>KAqLHQO*{g}h)JZkZe- z5Bo}FS;kUjwG3obWV@)V*ns|t)LXQxPQMr}U(Hxpx)Lq%fL4OzF=92IR{$3F!5BFu zbETCV7RviG_XPx6RDd6l0RRgvU3IoN8~k;`*;4O!Iz$^<#sD?~QUIF(34lbvt$=ob z!Fe-Y0W;`VokXxb=0jV4k8MP^ZA6bv9vd`D7iY8V8kHy0Qv*obE7`7C!4C54n zVtpwqW!m79oCRoF1puqLVhtW^gBLvoN>Ux3*AmF$ocuLn9X%}*4Io+oP6B(9Xu>lO z%fwSit)dyPEdVP377}g(6{;=9k1j~7EF6+=S*+Wqc1TwGhnZRX@^#Je4?qAwHj$#vyuZC3#g`F6}p3xb9Twwa<7uNXQt~K4zPwl z&plhJrOffVef6I9<;&++Yw1hsh127&cKZByk5FbHq)fY_WuZ$=z1vw==W6hJ{Pix6 z`s3;y1mXSx_0q78T)D6y*9sX_pIlN}Bnz&|mWK*vDXW8jEI4XW*2`_f7AyD4_lJ!d zvL1YP0bnwFnr^u@GNWj4ycSDgCGABMMzU{bwD}z5)OB7_*C3qqB(B7eB#uMjk(-NZ zDg1hhW-Cp>yy8m593{q*wtyeoZ!De$v|lJJ#ZmM#1<7{M0Pq(T&IbGC_1K1Y+lKep zD&$+k*XlOh9*UATMr^xmlr)tT=iGsLqelrVRjidSjw}fLXoO^8o`2AUgKqS;I9Irw zo0dBL6I5rtXlS|%tMm~3EatO8c|G!!uR?Z~P8%p%Ox#)9WsqnZ9`ZS)(8 zmaJX!I{zr?$}G_}{TOT7cl2iIDa#DpOI@`@ECnG_V^24S^k7WNDl%MR9B9#m957p+ zzzD!@`c*3l;0Qx{zS<*OcevH{&vkAdt@q)07(y-QG3agrPlLR?#$%b~>sE5&{cL3-8V)cTE1yR|pAK#5hIf}>MQbl&Px z{Vq?VW@&S%n2Hmuo?$g>y{ftzJsmmAX#jH13YYw-BGr0``l;XD?;Ij66ED!Vc+ry= zR3?p4a?Vi0GI5qrgtQ}qvTAa^a-Uo?dFXsBn!=oS4^HgQqwfoVY=E&ktAn;bN(_+c zc$o0w74&PA=O-6Xiuh!53GMBb5A?cjrr9fuNe!=nu71QTI9t4ie(wNG+2Ryv0O(l7 z*0NpZP7%6)oPygb2W0EfmC|-NUfyw{FwJI(ai%+B9dV7y$#`j-nmswAJ<*<|Wy7DB zIEC9qdiHvHgH$|0Gir&TNBGn2Q}dLs+oW-bPi}CON$rr%3eGHPv-$I0oa9K zrl)Y{?X^GJUnS5i!e{nd0n2q1-sqLTnU-&TonF+hWbKS`BR|O+-jikT&a(GpRdi=n ze3)Cfe@%Dpn7;qNjgJ{TQC>B1glwLEZPKNLxb);5mT!~gbJNF#Ie8$O4#fu$h?oBx zJlzNQE#M76uiMYV^W~NCCc3aP-lC^|D21^2+Lg;=(Y25f`lejo_aGh*n!(J}$a33;^q?K7#E-D0>d=v6Xh) zN_%XT-L}dzYtHAtZ(Ate-8fi|KboF5_3)Sv;>#{tGU8R`FR?M!ao_Ue#gVdQ$}l~m zx_Kigsyb`!Ei!*!b)AwGs*70bR2(Tc$cowJqt_AJ-fGiHkM@+XLURZadbd&eNKVdz zPx0OiXaTU=VV($oV@6)Ib9XGOpIKVvN3(}o{{u~1d&{Kty6~O64II9vBfiw|8$Gz^ zcpXlGEl__&T}Smm)x1fye*!UWnbTA6;fk;?5Zh4qd~tBw*AH}kd3fvBckR6J=+1AR zJD__T`&a@;wD^kp3Ctq?4e&YO0^kez*1UXWkNk4ptm^e3aJ_bfo-`}%30|Svb3_Fi zBHdJrr=7;-)9ZCZjm@%ozCC*}`kx0FVOuVb&o3;X9mMBzdK_A8i_=48_;Pge0DSV( z`6~-Fr9q2tAU&u8GKfC|bh+dW3-TzzH!qlI`7S1SXu%k02Qj3I8+JBXBVy0jry@HskvLZGkCh@`ULA*>Lcoce_LK)nejJz))L$< z&oY5mjmrO7H8VHls9K6wkZaYgcR5=eB3CY6U8M|>yH>9veSWpNSeY73 zTGJBSh*~#FA!UeVq=t!L7)`CWqG39n4$XS}9u0sB`N_>yy4t?yK%Rcfd%c0obwgR* zWF^4qk{wvl(&ykr;1yUiIe!0>Bh@*mUlPE9s|=3YJiHp43I{ZNxc6|cd)+~F;#lS| zMmj^i6HE1u{A}%5Wv@)B5A+6Zu&X}BlF%Ep@<_v|jJ4EVOPoW;l}4{<7b)_?hT^%^ zSp2nsxq#~cGYK>sS&oV)Cl{=U;IF}a4*;8^z9-nkWIQ*@nU2DmDvae7aiTL;6fMAb z(UWMfJs~Wt^x_Cp9+bx%!^z-pI4YEf*OU`XlPq1!50rc`NiHdU+n0*aQDN!I(w>?-01YzdDm^A>kCl{ z9-K@TV211ErN*+cFN0{TAkkQXX7M$PMd;aE=gn`zDw>-rl(X`orq_8xvnw_Bex;tF zE+ZX4KLh(M%r9nxR}Ek_pdGZ;fE9qNK?8KruX-2lzJGFc>JDC^Ky%29db=Fyu4;jV zHFlf1+%=}(0!Ew?r{HP8*kv8m)8yur*MbH(FW+%5)0bQ;hu@MXukjr3+lg`A6LCW^ zc#+f7w#?;k`TMRY3Ls-xD#R;CF#8C5kDF-yZh(l^F?bMoMjmi~Cm;50>aE!-T4yO~ zcVfo11a?K-DYuAGt_D!x#d>y54ol1fN;4EX35i7Ep=V!A!VLOn7ML$pooM-kBCFKS zfezRvhHSQ}nDz2z<4w#J#dZy#0p6is)j>016XOGRoo>kn`EFaD{I%ch5a6Q%{D4A$ zVF?!H3ecj7l86T00!(4k3;_Uth5!}vXsKb{=2mk^VCW@Lj}Lzbc+(o>+9NQ1oogUl#@9S=zd{3s9}i< za}NWJwG%BR8~K}kWEypj@R3k**5X`#r@POa<2cAibd0n7pvWa1d&zG}%yXM7ELRyN zqi7}m#})ohAn*RCjo{19;}_)C&!5?O;ne;M4;>3TcI=kTTk?5j_AGG~#Ex8w7VGo+ z#9lNQCB=Tc8f(NZq@`ASoobEOe=RNw!~^Jo)oJm(1aTwO!s(os+joP9Zm+~t5rVCj zgwAz*J(0|h>)QQP^Z98ot}&=g(PB5&x2Q?f8A*?SD0JrBK1oRqZoK_|i_$8SwifDz zLKkL7wIDWuduS`Vu;cL84?Nnt72S%a-%=-gvc7bzMh{*tpG<4zbJu2!!4f0PwguGN z0e%}{pV`=I!L5@L@<{;oXpAf>&XsX@95a`xhCYpb3A&0JQS8;6liR>>D_|R-9V1o) zRsg)90luSO70!(aFYK|6?3QKQw(6em<#he#wo7zDdw%DGqTFeC1w4@HS8?M8WyU;J%x^6>fBQ|&)Mr_RPt8QqxmaxvJM@Aw& za9n7Xbv_Zm3_l|<2L)Jem4CdqSebopXLIaI(;Bt3YM0tjFB}V+gJ&utSsxX(XIkmlegt@gZ;oxaF&Ruh6+Y zB$a*7q_B}YF;^`g-ag%TrEx?mJg`ccE87lKD?8=C9HfDs@dITcr-z~2=vV5TG z+r)wue(gXiFiKwDrMUz*>Hjn|0(c1Q`;1@IBU6sf)va-w)#5w4xA8sV6Xg!3G_-0X zo6P-jkxYV|gCKyD(*0=f9n3jqjj#L*9$P;Y;@d6y-z@Nwml2`wW54{@$EuWnmT8Zd zq^>4%-)CvAp_f1IQ2Oln;xCK%3#K?ty^L>Re4j}g=sjSC-@}dq)fDfzMAEEO1kzt% zgq5T)b42jhVXPOx#^)-)nnCYj(Jxx1`&fbX1?c}#zK=b1Y{0{pNtnxvi)##WpB9ak%gH0H=~aoNz0F7ciDrxR%D@i~rIluP@pc ze=QL$xFge_SldnvO4ZUEH>wWR(LBLTH>)kGcoDO5-sSuqMe*WUblJxloA+>5d)UR2 zmIQW-FrO};$pQ()SvLTkcqiYD!^N?$3hZryS(Z2!UFg-)9r38p{~y1ZK$gg=RsU5Upd{Qtd$S`YR4cgh3*<% z^)0Rrr%=UcYL-*Z{3@WOvM&0**kM-wBOCrB5dKR7{=m(jlKE31e;474Wxf^FFP3T{ z8XwmAz|6;2uIzcw=WUpG63*zHXgRl>Qz~d2`x!4TGVK31@oKVrTZ?n1=%)GUUyQm@ z9z0uKVpS~v9Wwy@g&Wf4H)mH28l=qZTGf+1wmW<5r5Jj-G*+%WH&)7XqX&;v7AgDZ Z^%PF*E}VEN25*;Yf^VOzRpOw|{|AInJnsMi diff --git a/gvsds/apps/gerp/views/form_api.py b/gvsds/apps/gerp/views/form_api.py index 6f2f081a..e00fd689 100644 --- a/gvsds/apps/gerp/views/form_api.py +++ b/gvsds/apps/gerp/views/form_api.py @@ -3,16 +3,45 @@ from . import ELTAuthenticatedAPIView from gvsds.apps.gerp.form_service import FormService -def _is_admin(user): - """判断用户是否为管理员""" +def _is_admin(request): + """判断用户是否为管理员:超级用户或当前角色拥有 admin:super 权限""" + user = request.user if hasattr(request, 'user') else None if not user or not user.is_authenticated: return False - return getattr(user, 'IsStaff', False) or getattr(user, 'IsSuperuser', False) + if getattr(user, 'IsSuperuser', False): + return True + current_role = getattr(request, 'current_role', None) + if current_role: + from gvsds.apps.gerp.models import RolePermission + role_uuid = getattr(current_role, 'RoleUUID', None) + if role_uuid: + return RolePermission.objects.filter( + RoleUUID_id=role_uuid, + PermUUID__PermCode='admin:super' + ).exists() + return False -def _check_table_access(table_name, action, user): +def _make_service(request, user): + """构造 FormService,自动传入当前公司 UUID 和角色 UUID""" + admin_verify = request.query_params.get('admin_verify') == '1' if hasattr(request, 'query_params') else False + if not admin_verify and hasattr(request, 'data'): + admin_verify = request.data.get('admin_verify') == '1' + current_role = getattr(request, 'current_role', None) + current_role_uuid = None + if current_role: + current_role_uuid = getattr(current_role, 'RoleUUID', None) + return FormService( + user, + current_company_uuid=getattr(request, 'current_company_uuid', None), + admin_verify=admin_verify, + current_role_uuid=current_role_uuid, + ) + + +def _check_table_access(table_name, action, request): """检查用户对指定表的访问权限,返回错误响应或 None""" - if _is_admin(user): + if _is_admin(request): return None if table_name in FormService.ADMIN_ONLY_TABLES: return Response({'code': 403, 'msg': f'无权访问表 {table_name},仅管理员可操作'}, status=403) @@ -34,7 +63,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): def get(self, request, **kwargs): user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None - service = FormService(user) + service = _make_service(request, user) path = request.path.rstrip('/') table_name = kwargs.get('TableName', '') @@ -45,7 +74,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): result = service.get_columns(table_name) return Response({'code': 0, 'data': result}) elif '/Get/' in path and table_name: - access_error = _check_table_access(table_name, 'get', user) + access_error = _check_table_access(table_name, 'get', request) if access_error: return access_error result = service.get_table(table_name, request.query_params) @@ -57,7 +86,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): table_name = request.query_params.get('Table', '') if not table_name: return Response({'code': 1, 'msg': '缺少table参数'}, status=400) - access_error = _check_table_access(table_name, 'get', user) + access_error = _check_table_access(table_name, 'get', request) if access_error: return access_error result = service.get_table(table_name, request.query_params) @@ -65,13 +94,13 @@ class FormAPICompatView(ELTAuthenticatedAPIView): def post(self, request, **kwargs): user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None - service = FormService(user) + service = _make_service(request, user) path = request.path.rstrip('/') table_name = kwargs.get('TableName', '') item_id = kwargs.get('id', '') if '/Add/' in path and table_name: - access_error = _check_table_access(table_name, 'add', user) + access_error = _check_table_access(table_name, 'add', request) if access_error: return access_error data = dict(request.data) @@ -79,7 +108,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): result = service.add_record(data) return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201) elif '/ModifyItem/' in path and table_name: - access_error = _check_table_access(table_name, 'modify', user) + access_error = _check_table_access(table_name, 'modify', request) if access_error: return access_error data = dict(request.data) @@ -91,7 +120,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404) return Response({'code': 0, 'msg': '修改成功', 'data': result}) elif '/Modify/' in path and table_name: - access_error = _check_table_access(table_name, 'modify', user) + access_error = _check_table_access(table_name, 'modify', request) if access_error: return access_error data = dict(request.data) @@ -103,7 +132,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): return Response({'code': 1, 'msg': '修改失败,记录不存在'}, status=404) return Response({'code': 0, 'msg': '修改成功', 'data': result}) elif '/Delete/' in path and table_name: - access_error = _check_table_access(table_name, 'delete', user) + access_error = _check_table_access(table_name, 'delete', request) if access_error: return access_error data = dict(request.data) @@ -118,7 +147,7 @@ class FormAPICompatView(ELTAuthenticatedAPIView): action = request.data.get('Action', 'add') table_name = request.data.get('Table', '') if table_name: - access_error = _check_table_access(table_name, action, user) + access_error = _check_table_access(table_name, action, request) if access_error: return access_error if action == 'add': @@ -167,10 +196,10 @@ class FormAPIView(ELTAuthenticatedAPIView): table_name = request.query_params.get('table', '') if not table_name: return Response({'code': 1, 'msg': '缺少table参数'}, status=400) - access_error = _check_table_access(table_name, 'get', request.user) + access_error = _check_table_access(table_name, 'get', request) if access_error: return access_error - service = FormService(request.user) + service = _make_service(request, request.user) result = service.get_table(table_name, request.query_params) return Response({'code': 0, 'data': result['records'], 'total': result['total']}) @@ -178,10 +207,10 @@ class FormAPIView(ELTAuthenticatedAPIView): action = request.data.get('Action', 'add') table_name = request.data.get('Table', '') if table_name: - access_error = _check_table_access(table_name, action, request.user) + access_error = _check_table_access(table_name, action, request) if access_error: return access_error - service = FormService(request.user) + service = _make_service(request, request.user) if action == 'add': result = service.add_record(request.data) return Response({'code': 0, 'msg': '添加成功', 'data': result}, status=201) @@ -205,3 +234,26 @@ class FormAPIView(ELTAuthenticatedAPIView): result = service.code_display(request.data) return Response({'code': 0, 'data': result}) return Response({'code': 1, 'msg': '未知操作'}, status=400) + + +class CheckAdminView(ELTAuthenticatedAPIView): + """检查当前用户是否具有管理员特权(超级用户或当前角色拥有 admin:super 权限)""" + + def get(self, request): + user = request.user if hasattr(request, 'user') and request.user.is_authenticated else None + is_superuser = bool(user and getattr(user, 'IsSuperuser', False)) + is_admin = _is_admin(request) + current_role = getattr(request, 'current_role', None) + current_company_uuid = getattr(request, 'current_company_uuid', None) + role_name = '' + if current_role: + role_name = getattr(current_role, 'RoleName', '') or '' + return Response({ + 'code': 0, + 'data': { + 'is_admin': is_admin, + 'is_superuser': is_superuser, + 'current_role_name': role_name, + 'has_current_company': current_company_uuid is not None, + } + }) diff --git a/gvsds/apps/gerp/views/user_detail.py b/gvsds/apps/gerp/views/user_detail.py index 2e24eabc..ae951e01 100644 --- a/gvsds/apps/gerp/views/user_detail.py +++ b/gvsds/apps/gerp/views/user_detail.py @@ -91,13 +91,17 @@ class UserDetailView(ELTAuthenticatedAPIView): 'CompanyShort': company_short, }) - # 权限列表(通过 UserRole → RolePermission → Permission 链路查询) + # 权限列表(通过 UserRole → RolePermission → Permission 链路查询,按公司过滤) permissions = [] role_uuids = [ur.RoleUUID_id for ur in user_roles if ur.RoleUUID_id] + company_uuids = [ur.CompanyUUID_id for ur in user_roles if ur.CompanyUUID_id] if role_uuids: - perm_uuids = RolePermission.objects.filter( + rp_qs = RolePermission.objects.filter( RoleUUID_id__in=role_uuids - ).values_list('PermUUID_id', flat=True) + ) + if company_uuids: + rp_qs = rp_qs.filter(CompanyUUID_id__in=company_uuids) + perm_uuids = rp_qs.values_list('PermUUID_id', flat=True) perms = Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) seen = set() for p in perms: diff --git a/gvsds/apps/gerp/views/viewsets.py b/gvsds/apps/gerp/views/viewsets.py index 176024c2..560a9eb4 100644 --- a/gvsds/apps/gerp/views/viewsets.py +++ b/gvsds/apps/gerp/views/viewsets.py @@ -18,6 +18,7 @@ from ..models import ( UserABAC, DepartmentSMTM, ItemReference, Backpack, SimulatedTrigger, CashReceiptsPaymentsLedger, Asset, Liability, PositionTemplate, PositionTemplatePermission, Position, + Workgroup, ) from ..serializers import ( TenantSerializer, CompanyInfoSerializer, CompanySerializer, @@ -81,9 +82,19 @@ class CompanyViewSet(viewsets.ModelViewSet): keyword = request.query_params.get('keyword', '') qs = Company.objects.all() if keyword: - qs = qs.filter(CompanyName__icontains=keyword) - serializer = self.get_serializer(qs[:20], many=True) - return Response({'code': 0, 'data': serializer.data}) + from django.db.models import Q + qs = qs.filter( + Q(CompanyName__icontains=keyword) | + Q(CompanyShort__icontains=keyword) + ) + data = [] + for c in qs[:20]: + data.append({ + 'CompanyUUID': UUIDObj(c.CompanyUUID).str if c.CompanyUUID else '', + 'CompanyName': c.CompanyName, + 'CompanyShort': c.CompanyShort, + }) + return Response({'code': 0, 'data': data}) class DepartmentViewSet(CompanyScopedMixin, viewsets.ModelViewSet): @@ -142,6 +153,26 @@ class RoleViewSet(viewsets.ModelViewSet): return Response({'code': 0, 'data': serializer.data}) +class WorkgroupViewSet(viewsets.ModelViewSet): + queryset = Workgroup.objects.all() + authentication_classes = [ELTAuthentication] + permission_classes = [IsAuthenticated] + + @action(detail=False, methods=['get']) + def search(self, request): + keyword = request.query_params.get('keyword', '') + qs = Workgroup.objects.all() + if keyword: + qs = qs.filter(WorkgroupName__icontains=keyword) + data = [] + for w in qs[:20]: + data.append({ + 'WorkgroupUUID': UUIDObj(w.WorkgroupUUID).str if w.WorkgroupUUID else '', + 'WorkgroupName': w.WorkgroupName, + }) + return Response({'code': 0, 'data': data}) + + class UserRoleViewSet(viewsets.ModelViewSet): queryset = UserRole.objects.select_related('UserUUID', 'RoleUUID') serializer_class = UserRoleSerializer @@ -191,7 +222,7 @@ class UserDeptViewSet(viewsets.ModelViewSet): class RolePermissionViewSet(viewsets.ModelViewSet): - queryset = RolePermission.objects.select_related('RoleUUID', 'PermUUID') + queryset = RolePermission.objects.select_related('RoleUUID', 'PermUUID', 'CompanyUUID') serializer_class = RolePermissionSerializer authentication_classes = [ELTAuthentication] permission_classes = [IsAuthenticated, IsAdminUser] @@ -201,9 +232,11 @@ class RolePermissionViewSet(viewsets.ModelViewSet): ser = RolePermissionAssignSerializer(data=request.data) ser.is_valid(raise_exception=True) data = ser.validated_data + company_uuid = data.get('CompanyUUID') existing = RolePermission.objects.filter( RoleUUID_id=data['RoleUUID'], PermUUID_id=data['PermUUID'], + CompanyUUID_id=company_uuid, ).first() if existing: return Response({'code': 409, 'msg': '关联已存在', 'data': RolePermissionSerializer(existing).data}, status=409) @@ -211,6 +244,7 @@ class RolePermissionViewSet(viewsets.ModelViewSet): RolePermUUID=uuid.uuid4().bytes, RoleUUID_id=data['RoleUUID'], PermUUID_id=data['PermUUID'], + CompanyUUID_id=company_uuid, ) rp.save() return Response({'code': 0, 'msg': '关联成功', 'data': RolePermissionSerializer(rp).data}, status=201) @@ -220,10 +254,14 @@ class RolePermissionViewSet(viewsets.ModelViewSet): ser = RolePermissionRemoveSerializer(data=request.data) ser.is_valid(raise_exception=True) data = ser.validated_data - deleted, _ = RolePermission.objects.filter( + company_uuid = data.get('CompanyUUID') + qs = RolePermission.objects.filter( RoleUUID_id=data['RoleUUID'], PermUUID_id=data['PermUUID'], - ).delete() + ) + if company_uuid: + qs = qs.filter(CompanyUUID_id=company_uuid) + deleted, _ = qs.delete() if deleted > 0: return Response({'code': 0, 'msg': '移除成功'}) return Response({'code': 404, 'msg': '关联不存在'}, status=404) diff --git a/gvsds/apps/ugm/__pycache__/serializers.cpython-312.pyc b/gvsds/apps/ugm/__pycache__/serializers.cpython-312.pyc index 94597729d5da6b0df43c8af577223ec91d156864..6223c871358a302191b18a8032f08db2573b8213 100644 GIT binary patch delta 69 zcmdlZaX^CiG%qg~0}!11Vw2^&k=KBkl{2(BwP>;(v!9@V>IClV0;(4UR5u7;5io7^ Y+&q^#i-Xhbf~4*j24+UH$pSn&0Kl>n+5i9m delta 65 zcmX>gu}6aUG%qg~0}x!`(aG}M$ZNpN$U51H*-wy1vBUd1kK#oh#TCIftk^0vIvh305?7ni2wiq diff --git a/gvsds/apps/ugm/__pycache__/views.cpython-312.pyc b/gvsds/apps/ugm/__pycache__/views.cpython-312.pyc index 93c56a0d2f84fb7ef6e1c908fb12d1aee040ec06..7a4c607635d57e068106ad3b881a177a84dacd99 100644 GIT binary patch delta 6445 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 delta 24 fcmaEv_$hIN057*(id9UoucM!%yX$5l-s{Q$b7BZq diff --git a/gvsdsdk/__pycache__/__main__.cpython-312.pyc b/gvsdsdk/__pycache__/__main__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9a4fbb0e86a42b95346d5f3ee56dd60b4ccd7f5 GIT binary patch literal 3590 zcma)9-E$My6~C)pNh@h3+rpoK;w3gN8jxd59txpxjolI(2PkfsLB*pH+KrK{FYm6H zN+WdyEkSPFA|_<41~V~30?ybiZ8M#E0&V|*_Cc=gv9fh0nbwlwsSeIddFr_Xy9DPFKz~$Pfg^nd{P)o7AD8?muK@2k`Q+O>L0fR2z!IWm0=hD z@b<%d@3Mov$JqSUdzkg)UE?FXNyqJ4+2nGSC~~>v>S12m9OC86HOfj{0dG*%%e3p{ zv7@fuzEiT;$?5`G(hgH)VAQYk4A@@Xo~iZV{cS#qZI zCV}F}r+tPM#c4U+tbPXLLL;`aqruMb9Ck*|#4)qx|BGh444uWfyyXcT{+$xh^v>HRG&Dd;)A~7rdAyh$cqwM z4T@8~?W%3LSDETXLw1CesCCLypP=JfpRH`EN^$)#QuD~ytZK`BTvwiXd@qHhUFti) z+o8(kUX4KYf=D_`k?0eKij_CU^{}$0n-x4(U>hp@tATrCh3EFT@dmo7Q7s5GZl%!p z%M^;@eSWbViz*3S!2S+k-quCQs7ps*F&)4Nb%^R05sQLBB=aams~Q4!3;(bI#DUVOR!GkKlO zr{2oX{WSmJYGLvh`Jdl#yS#V&bI;8k*RfBL8xZFf)G59m;jDYX-NBHWp z69T>qV}?z>BKVG1Sc_R$zdIIXOH&?-ku3Y8C&W-phwGSwwQB_YL8Aaf%pjc%L5ahB zW8Ptj#Tp+ovI6hsqO4nDWqKgwaVsQiST@$AVzaPDuit&n&$D~*oOXx{W5;Zpah1^6 zKA=|!C>V!ia(|}?VI5*)49lv}Fxtq!yBdO+LE_kcxWTRq)$D*t+RcTop7Q8`8z(r8j zDtjzoB4vGvJ;4AYg({i!^FdkbmH2=N`Nc~d|E4?QmmrhEVLr&oIx!?jJO`Nxd#KPL zQ#h#v9YENtWSugNn$U<$2E^eaw(eZi6$o)M3DbfaB*G6Sz%ZzQF zv1L3{oeNCcGUJ$M9Mhg9rgNPH%hjglDcxP>7IWLO(Dc${L>gMh`cggV+7+fI-IF2H zkxX}b-y+kzQnw@1`_5T_H@BvVmFl{TE;E|(r2R|PEnkwLT-SmxkX~JbgO6T{eonvg zX;t$=)vgqU7h%qDQwM()%G%neeRH*QzWIZF*>Cq}Eyot=0sLNb{Xjagux)qN+LmqW z%38j=NOzZL1F+BNeHCC?i(`>)&($_g^k3_LX8^CcZLBxdof^eqA4!j9wxx$NgHttA zWY*fc#O%gvZTq6C`YRpM8dH7ON76%CW6L7dxjbF z+7FK>^kFz05kLQ`a0rI@Din{v1HvVUFEs{&+Nei_=^vU76#*5cZ~K literal 0 HcmV?d00001 diff --git a/gvsdsdk/__pycache__/apps.cpython-312.pyc b/gvsdsdk/__pycache__/apps.cpython-312.pyc index 0e8cba19f30dd0e610da6cb35c31ea71f9fdd233..f9ff436b00145fb6e604125be837630603d02f08 100644 GIT binary patch delta 14 Wcmeyy@|I=8UPdNCy~!sS*8u=6IR&Kv delta 23 fcmaFM@{MJ~UPf*y7ps_HUq?Sjch|}L8P@>-XCeqz diff --git a/gvsdsdk/__pycache__/business_models.cpython-312.pyc b/gvsdsdk/__pycache__/business_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc38fda48826a3cec1d327ca1dd8eaf6448eb0a9 GIT binary patch literal 7791 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 literal 0 HcmV?d00001 diff --git a/gvsdsdk/__pycache__/db_router.cpython-312.pyc b/gvsdsdk/__pycache__/db_router.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce36e1d2bd616f3046148cc94946db227189affd GIT binary patch literal 3964 zcmb_eZ%kX)6~FKK*%;f{0UH9%mOVle$0>bk(rH_|rmY!HR)vxdlS-wXo}T?4!Nh;k z`<|1Tn#!u!2HjYhnl>atZK72Zge4)$hh+_u_I)3QMI_i$CrxW?!lw|ZG%25U?)zhG zaH472)zf+R-Fwct=brOB=e)nx)j1H1zQ45(zU@NjYw}?Z#+Yn;3nmwli24wXjw6v8 zK_Wd%>$`na#u8xCw zICNW9um0)b{Aih(59h~B`;NaE?hJRH;E4fzX9pkbdJQgrl=aJkAp6M;V6<&2!MI@m<3L;L6RG)=L5;FiX{~a|8VJ4%`O4dZnz~+igWJXY5jc5gG z7i~am#TuXv(GJup)&g~jF3~wc_0?q@fjYHedtA~304p6Kn)A2g1Rw=ZdAyzyCWq!V z0ABI-CuU4^mzTv$;~}dinn;A+E3J|pzbN$!>A2#5k$sQlIF21|C(FiYV~bONC``?j zjIET_Voe0A4I}tEsw(6Nw?EW<{I%{79P1Ych2-fJl1N~Gf`K@C$BQplb*vXp2MnOVXIBa8yp?o~3Ip;WsG zQMIw+(x$sqh5&2EqB;#|L{rIr>;z%3M^Xeu!80^4NpXEuCIy>iA2)ID80NlHD%4P1 zRTmFZJkiOEk)iKw&rPnJfdgf7Baj^WhyB1;o~DVPEwujN&i+MDaKRqjfDQD2@dmC1 zO_=m3wW-1lrP%jkH({v340wMlX)x&BA-geGecUDt!x5Z1;=5cVC#*J-1+XJ z=hX%KtC;LWjh+Wngi7(f1|JuZQi?sq6k=`0bePW4Su|KKsfi?Zho)mq`~Sw1dAr1-Ib74HCD>JHuv&Fd#SPj!XDszX); ztjOnM%7AL+697lmTVe{n@dn=&>+$(wR zmA^RdaVM6&JMa5;O@=-{ap}bLuDQsf@5pHFveP%oEHyu$hsXImuq=Cg+hIXcnIz%H zF(4Na1fv8DSTIJm)Hd-B;4sqP+$HZ><0FhRPz<^QG;V_x>;p1m(VT``VXj&SVo61o ziH4G|oRHW3DXf8iXm=GamF;(cpu7i24lO%9OU~B3v-Qfpd(H!Jza{V9ym#-ScmING z|FXyXl)Gj@!GCerXP{WbWx2}Q-<|H? zzA*X1mA1<-YU!`1M0b<{E#@e>*vNlU{LQ_xH>@(jdzXTbC_rV5u ziG`PjGN`*+*`&pYRx4G9RuTE6kdQP-$)u^2d8baldLnij3yM^=7glh*1>!RCIQV8u z)oYyeEwvoXw;Wt-Y0rDx7hLUTY0%Adt9D*9gy-=ypaK3X_;=GnoY)4fq=x6kR1_w) z{WvuwCE+Ire;e2~FNaBL=}AUlfuALWG-pg|`3%y^kQQ`Ie5Voe2Ox)mjG#w!z19Bc z0BUYsN6a4E24b3P78u_nH`=v#*xfWI?msyEHj@AV literal 0 HcmV?d00001 diff --git a/gvsdsdk/__pycache__/fluent.cpython-312.pyc b/gvsdsdk/__pycache__/fluent.cpython-312.pyc index 1cdb45829aac2810810e155116fb1faaa837f347..27d046138536ce8d43e697c32358be3c59181a1b 100644 GIT binary patch literal 46353 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 literal 34856 zcmeHw33MCBnPxXG0t83^1bAQIC5bwygVZfsmUURZ=tGVV#tuTUL5UI%xdF-&4cbcL zOrV$(k+Bm}V>yNrC#EKuu`=VCc(P7x=a|jT&f5hHYC!ieJAONsV`tyIkxB15>o;$A zzrXqf*p!bXJNtIGCI0BHuCA{7tLm@+s=xnzQBfg>V`tO4BbnPc?jPwwJ#5O!!f~GC zCOLr<_?(-wX{Z-R)(~4Ev<>ARUoZWOKV2Gnzeeh2vvCR zvXgvk^*hwS&Q_~b*fTUZ80m@i4Gl_;fgvH%AC=rOv3oGu%@SKZ5`Txp@00AihlU4Z zlJ&swfMnfu{E%e5wQmrh`-J2?FnsXA;fQ!ra^5x^i->nddL`TL?r21^-E%lHDB17s z?jMe{@{)I7U!-5yd*YZFiAM3p9tHR82cv-<3nloOL~qep=&}er>TEq}L0yZ(yAB-^ zBZs+c7jg z9PS(Ji-p4@<+&AYQ*U^vNFTr{Znm^4VH+!)t!zx#{;+rfY3L3cl`+^o5DAB+!f+V% z8}6rbPdNPGaCg6aN32HP3!GSkAF+x+Pz7$BL<;~O;}$Fydy|}pZkuDe4tMe~Em(kW zn_vaBpR~2w@$$VVFd%wjza7rSGQ12}FYKi;z*;G}`-g^(4j&80P9BT2TE$A_5St0K z0I0Z^+enQbDK%C_!zzuJs2>1Wt!f5Vy|dNJ61Gdu3fBG}WilfJ#}RVaFzLj}Bp1^L zg<$F9;@lBqUdoT-DG(^!QOTB^sN8wl8t41?XZf%22e=E?PRSAuiy_=YV;uO=8*{O1;3biut zc#3=4DsVDZ;`|6Oe1I_P%FBk!IR9M*{|7j6ITA413j3mcgV9*`U{53uh2kpY5L*Fq z&{q%+cMl4{Uoa1UER}{-^dLvLrRL#(Cx&=eBu z3D5|Y>)1wTAp-gwZKO*AG*F{7WFO<^F-MorJ9+y)e!*e2H_F*5Q_gf%duZWW+*veT z3o%uBOjsLDSX#ci9D)O|K)|xqaeR{=t!LmddCPTIl#5rIza#hM)DcWVpCdjv!x^4 z)88G9wiZYQhr6TQv6v`Q4aE%vngJwhG$u*~haxd{lj!gTn@k53Dj2~sCRu3#q9rFv z^9-wvLKv*k;eipap}X3U5Tblh02uY zO4iI2sdrYT%T{GdR-fMeW=ZLjJyR>67|N6^OYEN8vHO*qQnx=av*R;Q7EBc;Z@t(t zw?B?>R!feIbbj8|C z$vW!H@^jXyo1XG!N>+Tb&NT1KpP?SV69QnIyjSlr!zeHiiX!8b7c8ce_bNSx6*;H7 zSWgiT_EEdbI?#P0cLpMXN?Ev%b(OJea%bRgKhOT}?iz6?5QrJXx`ev$qjyQ)U|yH> z4ep=u;(LS?iliXa&n+g=L4L+2$&IYW6oA(>Pohg zk!}&ICL1V>iPA`|!c&V|P(LF}DBggC5ucI9QX#icwsinGD`%iSS(puNPKP$XSe6M~ zm-1XU=dC`sJ6ZVDZPRyWyd5cL$5jT^zba}VzYfL@i~G?e@kVMKF^UTFAl{7g+;xWK zzJu~z4GHC$z^;^MmyU#cGTx0T=f?k2kzf_NPcDIiTL}d@{1fut{b2|w%>;I)JUev+ zd@AE@PdVEe0%Rf9ozMDjL!sg|1g;~no4{TIG!j`?h*X(;nSlC1BtY^*lvn_g37qyF z3qgSSpaa0C1FL=Ge4*98X#qeUlkOuZd z`XjN(f!IK-(_EoK(`&IpTb#%&|T$ZH*|vT?XFv^oxX zS8X>_0<6LFBDJs8E;IkOZt+l5avXh#{T2^K9tuMXr(i|myQ5TF;6W7x><>X_5W#FV z;DRLy$QPx-2a;A<^;95HJnJu==Pa(~1ZHsMRKvMDE_v%`eRau(w6AHR_=+93t~j{r zR;DHK1`_VYsG?4wzlAm+5FO7Q!A1Td-ej&U5wA7RZ^qgm(O5#gbn{YGF~UrVpmrdS z;b#7BvE4k~v`%ilF6x>3h7WtYI@}%daHk%Z3Wg3Ifp!(W-P_j>)u`lvh86P-LSKLX z1)hzEC=FCa<|~Mti1a{*3T=>~vz5et+xkhHEB;x z*3+E!G-o}l(wQ+OzwTXYXv0KU-A$T2bwy{43I)6&cUUtY>}N zv;LB&{fd<u>9FWt+&!V)vrb0Ua`Cffeq_#xquMvPT8c%MT+A-r^F;~;{)Qw|~8E563 z+n;sUq}??sTa7&XkX1Xh_TP)74{g7@kJHY!yiT1jg#y;HB-E)|9uZ+Ly z#yiDc0wDrge?l2ejbwR68h|J97o|LE6R(+Zx6anqQv<7518dXn+LW#KDh4q%T&= zKft+&*qcn|BczkbJC2wTbzN`7Q%R&1$U8*8c{=O?mN-vZ?m<4zk6L2HSFpppMn;ul z6Bi@gv7?z2|Bk);qqZWTKB@l-*6-QW#Sz@glf;q4O4a*ut6*0#2Cl^yw;mybLf(Np zwSog&kMX$z^*La{dW5v-d51d7Ms0%?IZtYaQqhAaY0T#xqqevW@4P2&i}OcUU^MY# z513>6sy$#+dcZbnkC7&x9p){JYgp*9@K6uj2M*1~eQ3XFqfLI|A?}3rKJFpD)zvBS zVG%Xw?z+HB#eIW4{lh{ee7H{#VC*asX%a^SW#Vy?pGar7GNWa?myIlWtzM}Rh7K_n zeW)*XSaQgmg5(PmttEwV-$5_}FenY;O_Du4fKBE%kAp$1|5DNjd3w0<6^2j+O(%O>sg-mET6W`c-G8r-1>w2|LlGkZFZd9G1WG0neLcgmabU;N-S~XT%a-8 z@!a4P&L(_^j-?AyLceGabITRzS;VQ@%?kH{Q!1}d?!RcuG~mV9NwHzUJ9@r5v^YfJy2X zc2!<)<$hwlc5kEgrxp)E?^eK{x$FeJn*o3BEGO8&19rCZ;&JL`d%l>;EL4wC*M|sb z%R3tt4A9Le(L$MBt6OlE*f+@;Y2*-hqa7+SUl5RFSl6^QO|~I3!<2&=rV5y0%E=5< zE@qf2WQHlXPzdvsM}WsfSCQZWEN0#kUgj;~W8M<}lfG8JR1{{e1kpPq(c%8supf0O zq+xk^?2B)I>x&`TG#`@l+(8}*2O_b5erfEjC!c?7>~rrt@#D)coq7A>7k>5R^go{Z zy!_w~zjpcipMUd5FC$HTybUbCa3s2-HT3q&PhNiN8<+oN^70p7L@{r^{4|Pr^Tn^I zIp01%_U4N}eDlRqZ-40pwZwP6`Sj&yzy8*l@i%|`RHzMHeqK436O(VBpGIkIy?tUd zmUs89#MeWeLxU0AXZ34~f&uLo^<01WHB{%VKmCi#=YIIs+2?=tM~}Ug{L(vLI&=Bd zOKJsrY+4r?+#<*(xmb!fx<%wlv8pg*m8{_hqu?JH(TRdQ4T*h+`UboEl@#z+L`@_M zazV&}Z9gh`^YY|cL1f9kET=0q{^HvxOO@>drQk1|jK3K3%!byIO<&En z;D!AFlkfz=YJ#);!_2l1>pjeTWWNU{v|CBN}D~)QB8ATZ`j;l0_*g}&27p@2RxLx)Ybnw!h zB{_#gAtHtko>cY2V|Yi7ZuAKxjO2|!^`Sqb%=G}&Tg__2n$v;iWN#|4<|=I>)|9ck zdvLOp-sA}7$U9V4%CuEz1PwvSDfC5SU^Qam1GqG(48%!X=eI`9{Fk`5KxCg#0pB<&*T3s^C-l#?C1DqK-*Y{R6qvhvob+RXzZK zUV(4PiPI;}JaYPxjJH1Jte3?oBYC3{!^YKWoES9%Aam4`yh9ZSv^q+*eheL%kmZk^ z`|&XJiaAsZOV+DMUXX>nId5Qm&*c7z{f|GAG9$h=WM#F3?D2_kISL`_6wh>bR94<0 z)Q#MKD_c_%&e^@dfgulfN@jLA!?sBU?ysGRjb;!fV2dGz$78E+`%3@xtjZ#QQySR`8?R&LWd!`wV^6LL@95&|zz zmi`){@L2*&wz+Zp(Um+%f}U1vT%Q75Y@*gH}dtlzDTsA0ayuD@tcV%ZmR(=nP)()Ca zbS$hy{~Ki|$Ucfj@OLIXtrvXIGx~+~f*-#n0=&z+0zv>VD8Q?{t5hfjEEC{O-c>G? z16By|BJZjcDgmpQ&v>=4L8!sgHA1^ki{DzI4)^MWdcb-i1Q-$;02_oxz(%23XgbFW z&5v2SnuHdlGz-fBTZH9+%Y+qx%Y_c16(y|@R^qBvSOvIJSPi&JSOd6PXaig$tOaZn z)&Z_P>22L8x%V-DYbM;~IC^L#svme=BVaFk2IBn_``Mrj< zCr^^`;4Y|CNJ}H$M}Q4N#eoPBHn1!mSSG7eGJ(~M^QjAwU9@_(4*unr%%;6YB5AsObhZM^ zwW%+3oHv*sv;`Y^ohUx}3-hw!5;-(X(_${GwH+d{e=^1i&*HZTyhPwf z0P32~KWIcMtT{dXkgP{aa`4L1{t-__cLBhh;VGZ8J>x#>&Ul&={H!lH>6~yT_%Zuj zb$!y74Yj93?V0M11W!ii>iTSTTe`YUzViAKPMN%_(;Fpn)I!4bf{bUE!ROqZfitsn zs2Cf?Mh0^{#Vx(o103|rnl>5w=QqR@HH;c;1NT*eWR5nYl=O z1PPL*PhiMbj0xB`5{Vri5+s*wQUbG?BOAkdBoF(ek&`23x!5P`QyA&LM!AZg_7#iO z)iPI6J+Bh1$R(npa1+3 z&EBDBLk^89#0%hPt<7wgWQ2^c_4b;^qFm#GjBbF)wihy9PvBr82>g}#rTd58w0WT%q{ zqf|lCEYcx!3V(x?kzmg7RjTtY?nG(f08uWlOn7EX%O<-fy5>2jYgJ+|IbyUt>!0y9 z&H8Gxz9x8LB=-k$Rwq%;OT=H$5^H)#peRkuFzjd}cG~pb#jvttAXj6%kISIrh#o{<>LtSj2=)I=Wv}s8u6EDvY2Py)y6cHeH z#IZ3$oy5!#XE&V|HpLtkf;rBS^R&?%XFo!!gS0P3*g3DJm+R^_5L}COlc+vb1kma%9H00es0b+s|&#RrKDt&1}7Ewk|YfovUt4wx?EXpQ+w4 z+t`wA+?;OQoN3&8cK2LubMnCSvYFcTNPlj{`4v;UrRut5`*hQ{w!s!Q9X!A7qBY&r zK2y^UisbLOQo>c#O~xnU+48n@dE3Qcx_sk{)^z#y*L>T5GZ(1A!d+DQx-W>f6_tK) zr5a7TQpfoMiNff0!0gnro!rZ=U2f~Eg*?H!UA{dlEk9Z7CAf1d;7_;O3GQ?O{>)uN zaHVU{Ci~Ae@_;!C3?gncEf)S8z$AyYUiSC9ok)hhuLS;n8W~e3thfdQM_cXjxN?XM zur_Neotnz{EVK1qTbT`O?{!O*pykEdd#6pdXlUGk{w6c8g}BvV(a?Ek%_p%yUsvTx z^6yZdNNby{?I8L3g6yX#9-!2_0Hk6puE!%HnYEeEqLm<(&gO%>KGKu9%(ZO)sG*rK zgqxTNmDO-j43y7vHmr=hX=Q9W_u!1TAt_{+ZGCOoR=6r=eJyEUOHz1l;QYYF-I-;y zLcXCT_t0|Z!qZm@VKX%tQ#w5xM^qExP+KW){U;825O!IP~5tU+d9eh35gFei4t6{ENP3C$D zxM6M9pbX{ExpmCQmYgzZP_aX!7uDuv7=c+~`1wX6?o$qrOC4g~|0 zT};#3TEJ8-%o$YtYuuEFN|Mfc03e|uWWZ6?IlwNvj;8636^>4MLxvt3@f*nsvy<(h z@p2OPFkZ;tx$ZT0-CVHliCZAvl$1@bomiV(o+(+G*bQ^ac;s|^w!CKQz{E((*I;Iq z**WK_c)T;~38g*o;FQs1LA^eS?GV!s zPs#^PzgB+uOSqCd7UV(E^V=)FqsA6SVAr*@+lsM?ks`Y$U4jiaAveLlVWqkYwRZ2 z3U8!Ns=}R998@&H3g+IhV`9e>S0mBdoNNByGUp7t&917D#PZ-qOW2{#47e*=v3LYX zqJ#8fBKyI;5UrpCWV{q|%vqd3KjZ{c2) zNEhSz6cOkhT<0JLtPCU_;D3~j$qo3J8 z6{-2^k=+wGbL;6_rPRT6*Giw=e63*?S7s{P)1Ho5Z&{*vvCT^xf4@czmzu@@ z$~6D?GKxBxVGOQ;KSIOvsrNNVQx|%DBsi3jV8dPNdYRj&ytyQs*^oa&-7etX$0ym) z?p91xOx8`*W&8~Zhk-h8H}tDUol6r%=KadVykDRSq{9+dT0W*j*jG%2J?7WK9K}Yh zgEdRKm=TWh4&#$L9a5=_*}89JHfhZj{|H5*%NSuMDNFng0e$ky9(cOACQsGe{-Q^6 zX*awd`lz=)g*&IX*WKRPlGWpD$D1ctOsrsQYe%YNMYd#hn*MK^J`Qkt_Zua3N{-bN zNM4aF1Arvw3_gzS5H)EhR^S8Ujvc)UX^nb(jCV+%>&RI!TIL+OoGsgm<;p2}{*6#ox2$n_hh-8mOW+||ex4l?s zh8^89q+-l;pM;U2#IDC);FKaAIfY1XH!Toi2z58wKcyn_K_3#s20ZJ7Rn;vl;3=kC zU}%>Luw1965zk2svi7M@N)z!Lq@ey;r8oS7yt%r0M^sURjYU|I{yf zadz61uqN(#-CZ*0sh#oEn~d4<8xp#Ygr*2W6||0`Y4rQ*sNZcTiSwvxPQUnukFkwS zOLdL-U-3kK2mA(UBQ?ej$S-A-`ru|{qdwT4+XvgB?Ms*6`^vqk@_SQv+=ElO6Xa>} zVLgC?wS#(t=H|j4fTdRKkL_Klu)C6N5SHP5-xcu#ylTW}Y&&K90=2gSx6oeMr*iU| ziEEzNc@+^;tNf-@sxD>w5>=Okc~n<%HXJ=Y`uL+Mr@X&2 zhaeG#8J63wQc(fxY89ywkVif=H3EENb5-Zwdu1cxo*LIyBS!9OrSDwq_S< zb<<8vMH-`pxQj#@bp*<)L&-)V`zTmjbod~Pf+gBepk%+5b{p!7Wp#Z$fSck!0OUFb z*vQ6`JHsgDtAy!0keki0dCuzUP?g28OWx-1RbAZuQrXKje^qm-bssDxQ$n_GeY$S_ zYk~E%!HR5fc{;c}8*EDl+oq$Jg6&rd@cb1g=L;^i+nsgM^?2XMqwI11Lp;%oAIWugnw(2*+RbjW?;EFq#^LBvW`sl|3p?Zf7Qyy;;ac{DwKU(NE- zp0G%)=kU@E^H9TbR*n2;o;EgZwc51R>A>n~_}Op#S8AGVXt4j};(jWo=Hx5{l zp;qagkYbC#QjlDh3Q&NBrQGX(pM7HoTL#C~MI+5eEyy9fEaSJ0O=wV>aP7pk$xW%i z%G@UG>mh2U9uAzag`yKDA7?oxMJPqjK&TF)7&!755~vsv2-Pt-^iX$SOmat$_8p^r z_(vmxWEtwE8|0^JW!Uu{Y+K+g!9v0pRA4%tQ5r~%WE>ONABFwnL zvB#jsH=8BN55hcW7`(#P;@~!QTEpgrds0S^v7Ue;o|rQ(dWTHvyX^>w(RZ!i3{J2j{9{($kW%J`*PQqtK5>xpl!YfABGJ zQG5y|iceDujnw)a5^@b2>TZ<3B#eB69;yMzF+6LM!#3Rcq_tGVSVcKY%;)0jQR+yk z5&K$T;~x_4scOUnHY8%ShZr%BEh5ptd-_9bdGeVO~qn2c6c zygUT6yE&YA}kC6r{dE-_% z4>}AiumcN%M$1665c};lUG0N;#W-^7P68|%$@O={BD?X0X#eP5aMCWqch~;|y zh)pv9sMEZqZw5li;Byt{E1s)AU!Q5%aPj&VEt$aflxO=JWfciGvw+S8Yo~fM!IpWh z(A9c+PhwZ%K{;;IXJ)*sXIoc3Uwxr^+&$HkY)A&CZiIi2W(G~RCm&4qBr7lZRxu;# zvyNF$QCZ^7GxwdoZ_1S_YD`$>yfssICM&1+8RDqrkVuUm z3IW?olSpE>RVjoQsSg&hIZ_dYQhn({Q(&DYugN@Kdlo)YUy2b8+Xzmb^(Sf#ujXO=%OTo@^J4}A|4Qgkz+)jUJJjiSm^%M1J|H|YhfP_O;@tQIzL_ita z_i_1TwN<0MX-m$qT0&a0Dq0z>bu3X34`4F7a;cFVo45L;h5>wR{T1F z2LOzg@JU?fvV!W4>wlsOPSG=r6|~_@J!r(TvG8m33qE{;km&u~06%JDQGkp&l_$*N z8%6Ikumq<19Aji_n5^yMb><9`*a3Zz*;`E*CG!%@oS0P>0?4G%#||(8HuHDsv5`g{ zaNKs7xi;%;A$A-%%pw?JKY>SmSG>S1<`8leYdIW8h;HQ_$e}TN5U&ctE;R$>)q=2J zyDB)+qMqx&+7K!TYm6dlvRu!ho*8lm)imU6R?qcc(TDU^`n7tdmOugQQ8SGxMD7)8 z9{pD=NBUa*T0K)sK?xcgM|&|?nK>zN#@A{JXM70L=KUmSUaO-8*h}|h3Xc8{_}a_NxraO7PS7$t|h zQ`|3|?TY3<>#cvyi_c%sUMV<;PZzMzONl+Ndjr_?R`5p!U8C5c2QA-pQ72C=7bX=5%HU?S(C0=lc`y&C#+A`tk2Z6 z%L&_eXSd(-+V)$f_GfE0rfW9N)ND@OdAEFT=MCANx4*XYcDlDUU9&Y)vt7R7El+r% zQMiZyG_NQ8=Dl)d*O*rK?o9cLFYhOJlGYEHt>h<#{=KE#&r7TJZm|A*Jr8KU)X>Br zN}j)TT(ETRLl~wX;dcJofdMll?}y9nI5o8ZWRq=O1BY1GByZx>I7M?~%|2EOP+0ut zA7&;ooucN~TjnVgE0cW~pJF)##*ySC$&R>)_}Yb$r63=lT%{s=!c8rx#xrPvVjv@x zVoN5lB3GrD(;8Zd*y`AilbrpiX5XkOTVg$70U(TP5Ao_(%gY?_mf@AM$KB<`mwt8n zOQBD*z--&F>Fj~f<*$zY>VQu$Z`6bFQ1(J#kIS z6I#q!6ZRMeSK}lHgQlt)b){Bindk9T{$ORB=X1xY8sz|=M*CexIS~Kc?Da1oG1t9J ziEe;Tta0LB&?Ah^!a&px+>z1Hh!e&K8t*FE)a6fQqj|=0E^7V{kUhWoa@c}8ye3LK z1ry{*e0+Q~`}Ur|ZL7v(f{pr1%OHVS6zHl*t|q-xtQu21O!-(`AW+qaKNy8Fai z&@xPc4*a}Bv~OSQy1Z}=a+HQwsA_*r;BN^0Edl1aG(l%i5J(cZp8)v~%JCY$Lucf4 zBfdxAHw4HCFa8Sw9&e?+fSgEQC=$tdCX%5^B=d|&77mdV`XZ^J<(Lq}$H=`tFqzDQ)DlnuXEYU=F3<- z39CJ@P-sUKiDH|*X5PahQ1JHpd50d0!j8v^^-J|^!HMz~@bWU{E^l8w@2IeM%y;ni zW{OV{1TAT8dynJoTjufIsWOTx4}fqLtoQ|I&|WdWS}kwhS%4cQEYtjN9<5yd*yz|&-it@j}jU1WMQjYJc#nl*WYV#hjw@v*D-dbR)SkBJtHs8 zNp`4F?rexGNL!4t-i5mbM6mN|m$H-`#kZUyF-$Dx)(%cIZ_3Fb8+=NjXf0gBGMZ`1 zQdnD0i+R;-2oV`(oVD2D{*)boa}_T}m(<&ePEvjt3PMhwr)4nXF zH`FGQ^C@en_*tAu1(3;mx!kFl9)x?AR~{o0Yxh z@1bbdp4P>yFV&vm;KwLxsj<6Yv@dp+t2=c6EZezN9u5u6X_YX2ViDz*hLkT7dl|?1^ zOiKU(SPDCztNwVniCeG_i62`DH4ATM3p3PchYjJi1@=!JnG8=Rtvph?xZZk6ORzd0 z_KXQGuKpg~mZiV@q_S^)lz)ig3215=HJ-rO^QwZPiCYil-a`lteILdH`t-OQ=vk(p z9+|9c#uSoVVfmAPV6RA-hrkrEp@F_0nUf$gi|@yFV`m(jbdMm1*hhf)Jyq9Ev=`qR zU>|`9A3GY&QMto6d1!!Kg|^!E%Zp__dqfb>XS^-KYX=NG_04J$n9( zvyrVcGSO{HJDX;lEgH$KPCKh<#dKC8lF@&kZ{W&$`QN-(##J<+lK#rqgB7Tpzw(3k zOQ~#X;|J00!1EdZo}ISyRRFL0cWvDpu>RET+iSD^wAzC6pW9q}eFZ-+V&`6-&V5!& z57_tC6qvIc#0f4%)64{IeKanyv}ue83RaGsXUx`ut3IPEhxKywwSjR0Y)G=Dk_}TD zocY`gqJR(e_aDRtz7dV3QB!WDvA6>d<*)|y351W)iRSAD(d>o)fMHHw1(&@8n9C$2 z(+}OGU#l~E*u<~l&6N711$2mTLmIV=tuw6CHWUAX4($3ep?nB$Z<>xhpQgSB$ZbN` zR3RKxCzewWy#*TOl50)S&_0sF@($he08XFJEL9uBo4=1W%o;6%UEi%!QmE$96Ldg< zVijv}x6179s2%EkyO}S5Y_L^FUy;n2*iop6Pr7r=VN~W(s15Qrw~0$-AKn%lC<|@i zmScU9xmVq!c0kQ86>1(XQMMCVvh+ThUSct0IXbiuKbc|uRYau8{d7_Z* zMs~Vd)ZNH;&3GH19l6->!rec(|NHmPEWduXqH@A6??`@R#@{v@2u^PP>Sl_rbHmvi z#`n(ps? zSWXGr{Lo5Rug46ceb;82S=7=tQuq(6+53zVR6YkppOeYw6;3K2g^+m#WfT2vXt4ZQ zB=RK18R6rW%d31K4J)ndaLsrzLu!)I_c;#GNd z<$F*O>IdpEnOhgm-K=oxx17G^4?E|I5XtUb&rDI{TxDD0rddB6?i$nn#*DxD3fA_P zX$M7)DV`{vip}_%-^cRa!qTtUx#FhxZR+I*?0W#OI!ksnTVHLq?^;vP+3GP>`Y{sXU$rX;A%y(xP-m&B#}v9HcY?E!3Q~I3Wd!6_GWekyuU|i6)A`3E+qJVHs10 z_@Vk(#@ovl5PY+R%>h|SfQPjMrLkjy7Arl*X2go`?r1SqyKE8qG@gF#z_rzIU2xKd zFY{RTTyM@1kc@0pwW=db*DLlQNB*=D{|gdF3in*!7L~s&HAGD>L?X1n-fBarWh7|_ zv7Bs)?8OONBB4u!m{E)6@gwXgK$vBG(|D90t0ec)5fELR*Ms!)pF@xW@e8KEsb=^ad1vwq?`0}^u-#4!ADGC z*m4!av2ai(oWm$b<|~+Z=F@Jsv4CGuxg%)~$)E0a(HTuU(M^E3Jdvs(l44&Zv0Wsd zO(g5TRC4HeREP>k<+8#DyQ2~L^E-C-l{m4Raxzw?kfEZoQQ gEw})D|4vI8U-m9XU<2oHe&McD7ATMTW;UY!7gV*l>;M1& diff --git a/gvsdsdk/__pycache__/form_service.cpython-312.pyc b/gvsdsdk/__pycache__/form_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecb9fb833893dae4e0bad740736b8cfff2636037 GIT binary patch literal 19744 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 literal 0 HcmV?d00001 diff --git a/gvsdsdk/__pycache__/model_base.cpython-312.pyc b/gvsdsdk/__pycache__/model_base.cpython-312.pyc index ae057aecbbbc4e8ca9c6044f8f5c33c49d7a5c11..bbc381eae2b971ed5c25ea7f52abf7738dd8776b 100644 GIT binary patch literal 29044 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 delta 24 fcmdlYu~%Y)4>Px7id9UoucM!%yX$5@=4TuLVigCq diff --git a/gvsdsdk/__pycache__/models.cpython-312.pyc b/gvsdsdk/__pycache__/models.cpython-312.pyc index d499560429fbd7a11aefc8f246b95b4ed7ce82fa..529edcbda242e48ac276ecbd786761b7558073ea 100644 GIT binary patch delta 8315 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 YA6L-v$LBgmk`#hN`2TSMZ9Lxaf3rYjApigX delta 7441 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 diff --git a/gvsdsdk/auth/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/auth/__pycache__/__init__.cpython-312.pyc index 2c4778cceac5dab1d091c6c1e9db75900f9a19ea..d417f0ddf19625906cd0eb39b80a72d69e954211 100644 GIT binary patch delta 15 WcmbQv-^ah<1S^x2-sa1!M;HMsZUtKa delta 24 fcmeCu6swqEUq?Sjch}9&7zJ1WcY_FV diff --git a/gvsdsdk/auth/__pycache__/crypto.cpython-312.pyc b/gvsdsdk/auth/__pycache__/crypto.cpython-312.pyc index e7db059c2c545822c963db437ec3cce9c7f3627e..d0765ed96447517f93e74e7dab40bb4040c8d5b5 100644 GIT binary patch delta 15 Xcmdm|yj6L_Lq;YEz0GeJFYyBaGXw?| delta 24 gcmdn0yia+xN!D zdaw-MiU+UFLGUjq*di$CO&aM%JbI`R5%l6r!qyh?;2gg9=J9>+n>RDBM?a0KZ&Wpk z*t#}((S5$D?m%_(-9{Ra4K{8l&1gp)Um=EI^4|a3~y0Nf5AO!`YtJ4xT zGP5V?eAhh{GCR>g4q!P&ALB(VxMTD!&S7y7XQmBx$>SGqKyRenUUiy|S8LpC)t$Sn zBHZ#Fj~0a*r0JGW)qAOx`@Zd)wRYP#@3cI}Y_@Et;nOd|GijW4@iO#@XpZOunq9YQ zYxhQ=s{)9+w?Oxab@im^C%Me7 hVXfhmyaE&OujgwF*faniQS1O&KcrdE`-s87>o-O1mQDZw delta 795 zcmaDPbV7*tG%qg~0}!xB>10(*KPbn*=yLcL?CQrGD{kn2VnzM%3z9S$zh7EW`u~V! z4SNk^yy)a9%(j!AnMEfHun4g!gS1Ob{>Q8;&*N7FibzeSTP&f)sYSQ=TnjQ%b5n~F zb3*d7Q}ZUfup}~yO1D+!#VOe_ ziKQhOF_Yi1+A&U=tjlJ^`kjM;(_(Wvn_+DF;R2l zXE>_L?`qC`)Qk~CTJgK_FdyS)1d;swZi>vu6d2tUC;wyD7XpQGks6RF202(kp-5-4 zJ%>G`?&Nw7D^XpLq#=kf01-wYLTBO`R>jYLjEt-jMMjf9bLdMU$paPr;;_lh aPbtkwwJS28Y{MzZtH~(wi2+D}jQ{}Z#DmZP diff --git a/gvsdsdk/auth/__pycache__/jwt_auth.cpython-312.pyc b/gvsdsdk/auth/__pycache__/jwt_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe56851e87818e7086d667d05ae9314643a78d27 GIT binary patch literal 2710 zcmai0Yfuwc6uz4WNg$XIFgUFv0RjerA+ z4%XnP#VMAK+NvF@ROC^ot)ST2nf~eYPa@95?%>qcY`|Yqu`?Zi_1;Z3i3;tV*j8rg~Tj5Y(h*mO6x5}Y% zs~u`-tzy#L8iz)r)r{7ybEIQH0k#1$trL(MN->~}>2zeYYOOl|7Py)|a#@mQB||Xv&5Z{ghteg^g!hle1NF5PHF%{RFRv=Ae0yVYJQT&2Xz%w^J?El< z?nmQYKBATJ5~L+EIvBb6<5W+-CCMkn?9uqK7=(`T$mn$`o;I39+UqOr7ICn5>dM8) z=vAl+VL)J`pN_;1;6(`a@HC*<|3XOc%?lyU0Zpur+&mr`JuM!+P-%~&h`|`_gMkqb z2gH%jr-x4=RI;{c*AJ1=<6>Y0MnXLE#lwyciN2Iz5XWGI`o-Zf@p=z*F*@{J^xRkC z^-p9MoeKGs;WG{XQLfImz_-1i4RgsypTfi^BwVVL*Ku+nF43+?;X`_#8|}(AMaS~F zEs$7J3*^e>b+Ry@AycG6<{_1&>TF_l2qV?)N*%1fHsI3B+{veFL6Vj!mrSHQNdpbq zAVwPN!%krBsy3A?CuKWMa%MhTew%_#m%x^OMLQ6W*C8w=BB+v{8*DY1LFyU3JF^}3 zmANC6qG9oK;&3Tjg}$pH-X-&MG_41Kof~_mtc6^VioH!$3zp6oQ{#)4mH{>w19L_v zEnz{EYM~jlDBlOonAKP@DloT>!z&wPLr}t7UI$GE|7sA$wyUBHUBf&M|K7!+#y)5Zp znA8EGo<9gRMbCU2?HLn{G7s)-Bs>ii`8BW_1tnC&qFNC$pJ{l><<`oIDm#nHPQY*> zT>|5VJ~D#FyURs6c}~#Vy%bmNQ&G)UT>4%hwr8-h7?o?wWO3ozM?*Bu{?drwzFX`M{;D-P6FO%JE3_T5jJ0| zphe+DQu~(u#~ovnxlq`-6|fI=fcvKWu&FR)D(qvzMO#8eTP93p0nKFY(s1tTQ10r0 z{(goju=&2ZFl@Gl%+`MEgn4aX+g*b>aA4A0Fqvlw=9f+6y&0_fAe8grtWuF{ngNO| z(@Z*8TokC9Q7LtG*xhVf_uAggCpU+)OG4Qt;cQz7{VGS6-_itEZ--RCKBEQ3oR9Y( z-9Kq6>e~~vZk#CIbPK;@st@YxBN@5jjFQ_KCHM4IcMVIzhJuixVA8laY+M;Ku7pO7 z3;xI|I?rEfztBFBweGQ6VSM#T8pti3%vl&Tub9ZO1=nqRlEj8g1v37b$wMX&bR7sA zN<)Uy#{h#^J7}%ilq<@>@i@ zU{!i!`ETP{!oxAr#}%tVfEUTAC2wO+Aa9>)--yL%6T1p7(M`zZ!wE(h!+rx>MNsiO YsJRC=+yn3gS7158f;E2w_>@%q3u_TGA^-pY literal 0 HcmV?d00001 diff --git a/gvsdsdk/auth/__pycache__/managers.cpython-312.pyc b/gvsdsdk/auth/__pycache__/managers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1f65c42453a4285267da389f0381fb7ecdda455 GIT binary patch literal 14947 zcmds8e{d94p6~9Np7iw0WRgrKKOlhwFf!nQp6-gPqCj*7G$MpmW?iR-OaqCTOzfUP z6T=;>fRhystUn_DD5I-uC|EbU-YEI^!gVo;2_g084L=#F}V zo@iyTl4UsN$4tb&pNa62?YND;C+H=$4ydg-&S`a3q)vc3r%_i;>O`n>8Fe)SY`}d1 zALy=aiD@mym=_W~zb}LGEtWOn9W&4(0v3^y+ z9<;P6rgX=#SCwAsl%oB;;kcyEv(}i#A#ZD!`r}>nYbez|jhShI`%y+>a4Caagbmsv ze8je&4ca3PSR6LU4{(7B)n)pS#NWa*Q!k89CO@2d@6D;h2lGeA|9$TZ@NaVD@VQT3 zrA_(dtNGKX&YgVj{09f7PQ7cc{A%yd#TK|GU;4x3`!D7XKYQ-e*QZ`Qdj8d?^B=#J zKk_lwoPH;N;G_I|e`sm%3HR-cwd4;TxbW2Y)Tv+R-+Kj~@947A4~FKrv3vL-POcu}dvIQsYlzcl#4^ysV`+w4m}O;Y?Fqa2xh+hl4=V@wg((=BlYf3g@iUGRp{5d@e;mHbxTjg<#32hRe`~wqk@b; zIcaJ6bHmQvN<@h~x-y)I?^+q98l|l0A5dLgvIII1!Y2mnasHaVtuQsXVI)NXvX}Wc zZ~fVtrd0FonVQup_v)OtF&Q5j93Grz_$u+Nzi~8pB>4QpS^u?Z|Ft=PbFOawFCTqb zzE}aR(*on2f6)o0r2V3sao6VDRatja+THZ>LucG=Il(>R9Cp5Je?vGbB%No3)|}Uu zbj`qPnJ&$~V3|QtBjS1r3Lvhw9z$5MnO+e#!s(U;#H4&5Tf~0FK77P+#Xc40K9^Gx z5Oi(_*9~()0SECDOyXzOa%H%)}k2&;0Jtr&BT-*iT}-smvq8IPY4 zZpwKVCgWM}!nAkcm@vLH?Oiv~3nV41ldpwW1h@{l8h($l9iSFMD1;gs3aMf!1bUU| z#j-mTdMpv{)mkb-p-8MN6e6E5uY_miIwUw6`CQqHB_wE_6d#cN%temn*ReAW4_|v} zfrD>nX@Bfh||GumXqD|B=ezkXBK6&uM zfoGvG`TA2Ajvl6ubaWXe9|l7Ufon1I36N2y;6CE|3Nbxqmjk`<(!Zme(*S*8hU^j0 znju?+iyN+^oD(VMCiJmS*p55t+rX)mbr*V1bSI#f_5^+n{d&3*@-nC(au;yYn1UF7 zu~X^`_r<9bz~v=QfW#b~1O1XJlIVk|sp?t-G0M)qEnP8a@S1K4Pq#}-mnyC!_PDb< zDg`QJTr#;4$vh;eX{rzbdmV>%)scvI^~D~So1q$=1ioIr1v}o3WHpjofv9#FCTH>o zRL!+l;Uov^km{D7P62CBNt8|?dzniPrmEIz;Okp+{zf$O^$od(#kr>CIe-1x`i0s0 zrRn;m+4_~~`jwgb8!q`owE5F@jMIZA{*a4UcA`>6mK}Lar04@Ed`D6Q;mpvtkskKA zCg*!>;6=&=8z$+of%I?*KTQ7I$y0jBhPp$=D8Q?3G^Dt)$aWdS4|x?3)wM1b?GN`2 zkm@_32AGfUwJ={^gLP|>5T@G;L&*0)t?ER(ACln(7Mm_ok;7LPGrLfa6Hzt*A71@)oHZkr!7$usp z_%$d1i-FRsTwK{>HZfX1FbeDmR(Hw36mDM)Fs58j?t~F366Rr?Yq@mhJq&Jij~xtz zJz{&boneR>Ky{mX_wYwY9-tAC6o>rcEf_*k5cS`pjcBXz|U z)v=?yH!jJl0|GxOE(IKg=P1{>#}u`KHbzu7RH~_)rD$xoRLZ9ClrI9#3c*1cRk{A5a)M;T}i`3(l9;AL}TAh1u1*!FNkEU=F&%Rbh`J`VbnVP6Cy zxD$Lq7))NmJZG3GUS;26JK@l015B?V+I#-F5A!208BEvM%~H1A3nLMw?L5Y+_8pKx z!mnG`=}@S2Na9ydVeg% zad_qKn2hFQa6aDY#@f<|uTy>oq^ukeDY$tyDR|__BV!vTIuAdR@&70(p7r`hhK7f- z-bHEeq6snOU6k?Oni6iMVxch+UdAwnSd7CB01Xg+_cBp-56d!fY$I1WH{YWg1KTtfqYz#7$lQqvePm#_kU$Sd***EpJCm;sn-v`OeRwZaQc0l7 z4LnCCfWo7_BwXy2w?Yw8K4RU`gcB)nsC6XmS`qH=S5|bzWN8J-{S+DbV-XQ~8xCQk zgJQcX5>rkE0xhNyo})bA6i^;e5=!L3-1s0osjLUGm-#OT%T7T zziwhz%70zPe@E88G40>@`G(Zb!YTj8jDLGl1Rp&M?~SkyhW>uIZIlFX+f>QjtG!B0qBSPah4VBgd6ToQQaA;=={wmT2{zFdleu%)+ne#fd0M*b^UBIl13r z1FEQ-E&x-5O=crtZN+R%!J()XX!?s>d8L@-(3QJJDuyc#Idk6Xr1SgY$SHRXM@|_W z;HcUW1`zX(mIX{S;hkZ{<@>SLVd_ixjMiIz0Lf+`CBD0g`smAX&f5AYj#2RAO-@-XCl3u%wLbsp)03y)2IZDfd1N96C=k@RlML_}r@D0=M+;8@XNtps&$mbm8gvy+tKVsc5Y??f{@dLZ zmEyZ%5gDUY8B+{-3zDBA*^Xo6`hggSX3H&MV0NKxcVPF48zWQsflCPby zF^-yPEKRpS=^`icjanU)rW>H{YtF&X*Xp1&U8U9CX4KtA>MnA2Uay1Fv|Agm#u%?= z*6}v6q~k?+-6jn)!g+VWpKB1UEe2@xYRg5(--2HXHW!*+=o9m>CW@WS(_%A!NG$v}rRhk4-r^ZQ;vjOdMb z&j0qUqWuL#&0O#|GXyc+ca+v6IE26uJ7g~xLE>J5ZVWEd12VlU^njh5EA(&_dc;MY z!97MvuYuHK7T{p+PlCZis0uCuZQ(jwE?o>XXiG3bTa=B&uu=A}i9}>cQ3%dOYu3g3 z;^D5i>O|?-v@6yJyKh)TqzZ(Flu7(Uq84&XAu#N;4)!@{7Yc@9orYN_V3zW3B#$F` z0?C~~RB>xRqFV(05@Z4EE$EkRrxdrMAP#vQ_9T#CDp`z#)*6f2(l$U`Jn_7x~0pf+|mjQq+ctboYj@>@7DAVwRTtm@aUDIgK zk)E;nqwf}!kDC%=3`nYj{Gu0ad`kAg6`@-FQ*g~WWf^p0> zEfQrFCe2yn2=YN3#wkOjA5vhH;+UNHv0~9FU}-PHlZ7zuGFaLdO>IXW8(&j~-LvCq zT95;;Fy-C-a5J|s&Wxv(16gzi5{!&P#4W;U7?~}x0A5QmGRh^`vDRG2EBo`rC+N{N zSy7xi#6yG+mcZJ)@P%;N4i7-)OLwM7)KI+S1qAFZ=&7KyCE8lIpg7I_yn!UTo3s9H zY5%s2|1wCI20FkfrloxwZsxYM1*EIo!lILcO=*ptdsvfhnORIGa~brC1@udsGsL`( zL@RVQ$GnsU29Tj?4~cM*9|hz>*B=ePvM^{H^d;t4V7bJLaxBEFjBm(v&B5ba@y0DYT_ zwv-FaOi&|cYnDrY=N4wb9^exV&}`N((5!POPn(>QZtc)@$Rj{tpW4C{mEX}V0}f2W z8)T!dRGh37v8D#}MfYGj*0;c-PnPz-gW(j+t!Nr(-3GXU0Kj);JsZ=WjTz5x0gPOHz-`b3?J>_l(G;kK4x5FF1JV_IdWLqG9D91|H=h`paQ^qB=RSURa{SH7*FUAt7NxPUBVTw_1YwDm+he1{rlOAjp`~TN4L7$7#(T zv{V$|FYg7S^5g~bJ}e9U+Js7y(Mpa!SJpqKQ>skr?gVKk)=2(eAICGb8R%V=!I zfAbdR<~QXU7vettSjWWLOykX^*+yv%9N4H8lL8*ts2=eQdk%SX-uk5X|Jngez550n zk~KR3s&+&Sz-f|I9)o272S6BPH~@%`u+i)S2(qfLKX_Amc4zP!>q>*C(5o;cqTMj2 zQCfgdXV5?osAauvX>Z%aG6YB4OyFqInHJP|qV=`kvDO3hWxEpeHFwRO`;(ya7}>zJ z#}3fj?4Xn#d_HeF2Wd8;nzrl5vs3_i^*f^VmHM5g=HoEQA~i=w zU5%Ra6mc@tT+{EkcA{KCpaZj3E&Qk}R$qM4_1bc}ZcalnI7aAt24l?c$bA8{WD4Uov4P=@YgRys zJ#MER;RFCT8^Q|cCkKMYCGbQK?D3kVjOJI`MxY(GbUO7d9{CvbmL(*20s;64J-Ago z)?nRQB+b~#rn+^u5t+otRa<|&U%|CAB%-#kW7`?i9~fL>m4|Y@{u9Pg?gB!j!5cVR zKYw)a$l&us+4?}bK9FnuD>$ZA#-=-Yub8yKj)v1cA`Atm<}4STm7iy90mQPA@JzJbA%)j$YK>aNfFZ%+Gf&iYrU{i}f=CxvMnQ+LMjMIllt)}5vWqr|MD+!^rqh4pNGKlV zS!iaUe|Y)fp0Rjxc}Bc$8u^GvW9QAjUs@h2X@~dCjjLv;grv<&U?`e4mqtxyE)>O1 zC*UE7oru>fXw5rNYmJ%BJZy*BGzk@l`Qxh%Ey zCn@iWjQ7Em@F2BBXnFL@4h3R~c0gfn>r{~b%bftlTjS_ypf>Rkp5ci zK%X02WR=;{K5ksv>tQ&uw6#mJzU67(@~m%V+PCtvhf}_l8Q;Z3uyZ2d{3)p9 zh+1sc_Us$9{q#z{VVdsQbFg8qRbp{kAXtWn*Ly-~dI7;6oV-u#2qnl-5Rc#ud;>}v zM!^?G#G-**JXCnkL-aj}kK97WBqg#wWbOeUb@yvv`s%z0`M-2fHM*pMB zYWXwh3?jfN4*{9Bu`K%~27z29m9xHbVtBtg`IWQA{Ej75LZQK^+$L1PeQtr@T{}$NU{8~Qg(3g&z^H%lcs4a zo|$*w{W$mBbI-l!>;9{-uz&zO?Oqpf7ZdUoHZq`}s!aAntaPOVEOUC=&GMAE?sq(dh#he!u2=%Cd1uwK1d0!wU)DEnIdf)ErBbV{OciakmP z>nwbec1kA7LSS$bd0e8p0l_IP6tk60_y z4rj_ylW^3W`B~DjB4$e4oH3h(2i^wE!3wXXOEJT7>xAOe6kh28n@R5jLC@7!@qMg^wsh+i-sY_lD0uzIBJ@V!mJOA3% zOV5AvVeDG$6;E3@1RNZmlSu9|KDq=hu^(+xPR=O*T#M`GNJ@ndo zG}(Ur+rewG_rB`?HQmSz6}*G#pvoax;FKkLRW(bY-5k76y$f@WZTeiZW?&)CEWrYA z>Wr09TTsg;p9G>{%^9%Zqz`N#{Fy!$j6qh(0c#2ZT&6btSMYjL$SSrzs+XA_otZ>+ z0c1W8qWUbR!Hg@jK~tBx4}3+`7%0s{hUQ$WM&Wi388RNBkBhPa6iyxlP~Y+ZFw670 zpc!z_?#ps%igE!IWgY|;<#j+)&H#W$_2LUxFa3V}udn1N9H_s92ipctyr{W*pV%czoxyGB6t1Hg z1aHrQzM&GUW~j6)ok~OoWTVpS5A6vmy@KtX!HC!_NnuGX+N3Cwe|JR5v}YPGX)(-Ix$y3Q=u>3X+7uS$wy@HC&@U@0 z8wwv#O}I`k7&MiQC_c3Scx?^|+n^I2T6eJ1C#nl5(fZErZn@qUmc%K`C@n?e7ThN6 zgzlh#x~$A9hVPEs>yq}?SmT6|INT|FO~PKIp$wL#y!R)( z_ouuY6W)zU`vWm9Z7Djs;qZo(Wl_SiXs|SHS(LQYUvR!x^-k5W{z6sq?$*neR>}cg zZY5NYuRFFb<)}_Lst4QO=FjqRM|IM%K5kwA-OVymQgNy+ZmYdP7_;jNFwVmN`-uWl zyf9^3p0F*?8z?5lr74>yVe`xwxGk>&;S7~Dt{|7Hn{?z;rnbpRK3!pJ+{k{qfr0Wf zovo>q{mj8&>9jR@+0T|SP|mR1Xo0ESei!UBM&S^0L6-)!g=#@T0DG<&GOZ{hFd2I( zHGq9LW=5v}QuW;Zfvk`m2CvHpM{o z?7q4864iq@newcGC9`K`cPYySgGCd%*@ zw}JBo=c3wjd@E*ZSieNBo0X+#u>=hDa?8Z7Zl%ZD+c+N)NE)Rc ze`s$uNmxs3xHhGY`Nvd4W%r2(y*df6i)zXY^e&%|H1(D=Bia*=gajbxWu!)!+`DqV zDYB?|d{enpZZ3m+xo5sf*>z^*H-4FBO1+EbnvzMifS|U*_>q0&+>e527bQS`XbcKc5bwRAVr_cQ2f=9@ml3 z(Wr*qJ&Gt(iAoRR6ihE9yiKZck3XmYwqcK7g0vnzmTJjK;8YHtTtaYi5aAv4iQS4n z98wK*Q(p6QAfz!^h+Cmi4-d3d+jNsMpqi-+Y51pA>DKkMWJDR$@Ju|Uy(G|_ef~sC zdq`Ld(_{whBs?TkRmZpylRLe%7XLL6u@sla^rII0!0HnZjaaG&xwoxvS}!_3cE9gV zt=WEg&GxjT90QQUO<&?G!}5e<`QYlLV`Z%Azc$CrhljXz#h0$C)3%egl&d!3s*TsJ zzbGYL55@{dO+`m7hb{5a$KsD~k3T71eo~72r4f^?T3i#l1%=y~(Z$}u70Jc-r50~U zEZ#6+OS?U%TTiwQm`95j9Jd^^#NC~v4fkH?`DDpR!{!q$ICg65us`jtO}QHq?uMlM z?wbZ($;z8XQn@7Mu1mP5=2S&nqM|KX z@z_Aq6=y}dVdV*K#8sDedQ#51%g(y=>IYJ*wUMEK)ub&Ntk^~6?frh-)6vWWC81`-5+P}cO8Lsh1_2f_Xps@B>LT95+9g_Ns2c5j>3Y0qFA zq+Ni5j#OR)1qg{@z90?%A>J>@($7iJm*(=AGi5GMn9EPxmo(S(8*drSjO$wh#VuDG M!$5S6CEeEl0f)j^XaE2J delta 1010 zcmZuvO>7%g5T3XHHje-8Y`jZ$T^pwxDb6ZXO;I>N2vkUrl#qZ560RCARZUu#x0|++ zwyF?4AjARdUD{LN1fu*1^@6wnQcoa679(x4q5?rBZq!aI1##lNM9zsld^7XS%s21N zzKw~`ipd{_hT=fKmoL^^FDad*ik3QK6XP5lho*21n&M54r2$@%_c)1i@&Tu;E1gfK zJAeFz=iWsU@%bZ2CtvapOY4FqMg{U#y1=c)ZuNNukyGLYG(rNg!Yy&+ z^M_2;6ZdBT_^62n>i8b1CyE^YlNq8W3gVAGYQ+iSa^%fKj(nXM(m0>PU&O#m9SCqY z(Z1g~l}}#bz3d><%Ck;1OkUvf#7^Y7mP*~+5dWz#nAp;Ynbb0ZFW@Cl|DQ%yEe;0k zS5Jz*h-F{&QDe{mzR(&y2o4;aVf^0sWGXKB(5lctfAvMEfM*WMs(^U}C*eK)5?tm_ z!X;!$^O3OTIyg!_wT7{C-dk+1I?Kz?&P8Bxk+Muvdyz8WEre@S&&rZ}nL0W|Mzcjb zPYGOLAW?|K)9aq&VuAT``*h1$@!Uvy{p_meVEhPW$(3wLjXiyN#kt^}ZJ);p%2^Ul zQiF%dH`(zNrl*JmhAxvNv79ZlrMUtkPvr9A0}1F~dM#JFs#i7^ZY+fQWS~!O+E0Ev z)zzPQ=lSiF-s94lkzZ>QAD`>i9uI4?L2dT^g}dhA(6j^74$bMnoZc!Q{z(5&56ee` z^3kob-4mgF>^|`2;;sb4ddDGWdNLG7`wFP#yGCWhzu||*RA5YfwXkVSb&cn)hQ?xG zEOw2h>%z8XY&|-C{rE4s{kv8THT!4H-kO;UXHEn&Cm4OKr$9O%rpzE^elXlkS(}RW z&jXoi?*Ph-Z0FS;Pwl^Mj$ZXUeYQTQ!EG%zH^$$tA&T>KNhP)oQDmw`zTKA=rIF4Bg8x-cP82PDCN=L~v7u}}Ye4~D@-E>x6 v@^_(1>|*uEW3-69boUSidL)GIz~paW-GQ-PDTdTOP}o(gXnY4K>?!{T$aVOS diff --git a/gvsdsdk/auth/__pycache__/permissions.cpython-312.pyc b/gvsdsdk/auth/__pycache__/permissions.cpython-312.pyc index f09e5a47ee4d9a404b0f03c38fabab4bd8c8e7a0..6435672a24c9e4dfd3dc1ac7eccfc0ad5f265ee9 100644 GIT binary patch delta 1142 zcmbu8Uu@G>6vprIzr>F1#Ep|B?LwhlMUzR%%Ekk*c1108RESQRK$`)DEDmK|-KN(r zQYBaG15-ijMA?}%_A(weO`94Cs=icWe;)R@O=<+FB^qLrD1w*KiM>vmoi)w%F+7&hN;f(k688L|oqr?IOz}L(uW3dKnaRz7c z25$+5U1^5=1#2M(s*Q5#9hhIxsNZ}o+hdIh&EHs;Asp5#64G`k8CT}v0 z*2lnPrg<0o$vm9?g^+2%k?;-Wu<8mGq0#k76kC;+T)+)bnCo>p`lSQ^#c___&<&oV z%ZOsIq!In$!E!)vB1Ai9DX zsK^*oI#_A!0d^}#K*8&BN>iWVLylhOLmnMoU~o}x`5z%W37?3{mB_ZepR=d9)+%0rH|J?s;pmLe)Qw)!<%1WCEUl;Ht*>0 zNw}A%dwrFQ91yxaL3^|qqs(RO2 zvST%wttPV@EQ#%TN|=~>@z7raiL|aIQrG0q2Fi)4^CNNlnWXGbzf;wjPF+*qlwXox+(h5@xA` zA%ubX96Cf`hfeOLJO)9R4h4ZCtn8((T|&`6pc&VtJOsV)zVGw=e!u4no~6!%j?ht$mBnB~{KuplVq;$%pb=qWf#$vo+<=PUgv>OWQ-W} zA~BdezfSwv2+@OwDC?ozU#M`120WBsr;e1OAsWIW=+Z(^lYUA*s&N%2W9O}RAGSd~ zW%Os$H-^WD$40Z$4{gIX?qzfH&P*0xQ~ki>4u!yO6lk7;D9*F1pltA5mV(xeJbxWP z!}q}_(2m{09Q5H^Aq6eCEeujyG{(dw87`9*m|P>I$T|Wp%U2keEwLDrk|UhUS+G3h zGS+hks?8GOGES^!*G904E;HLuL;w5+hpv>S1M7hOp=O0QpcJgd6a&r)uiTkHz-0}t zB;e_AlIHJ!!)NkUTC7HdrDGAn)lfGZ^u#jug%ZkfQgE95bgWPc{cpJj-sLT71(x3c zS)duR09ttd7s2W9N4yvr!KaZS{1O@9E_f4Jmrztr!YHPcvn@VQVej0vY{z1K@{0d( zRhg@_frx8LTf=}aV{(pTRfOE_+|(^I=SP}_6Dp3MqEW8#fFsRGoQQSctLR)j z-f=7@@h|oH&NOIj{q9X5Qp9TX8qlcmaZRp-jy5c1J}~kH+w#{Lw_?{Ijfwh1(}d^x Zt6=wf&n^uB-$|xSGMK5?_(7 0 @staticmethod - def GetPermissionsForRole(RoleUUID): - """获取角色的所有权限""" - perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True) + def GetPermissionsForRole(RoleUUID, CompanyUUID=None): + """获取角色的所有权限(可按公司过滤)""" + qs = RolePermission.objects.filter(RoleUUID=RoleUUID) + if CompanyUUID is not None: + qs = qs.filter(CompanyUUID=CompanyUUID) + perm_uuids = qs.values_list('PermUUID', flat=True) return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) diff --git a/gvsdsdk/auth/middleware.py b/gvsdsdk/auth/middleware.py index bf92140c..609769d8 100644 --- a/gvsdsdk/auth/middleware.py +++ b/gvsdsdk/auth/middleware.py @@ -1,6 +1,9 @@ from gvsdsdk.auth.cookie import ELT_COOKIE_NAME from gvsdsdk.models import UserRole, Role from gvsdsdk.model_utils import UUIDObj +import logging + +logger = logging.getLogger(__name__) class ELTAuthMiddleware: @@ -20,35 +23,77 @@ class CurrentRoleMiddleware: request.current_role : gvsdsdk.models.Role 实例(或 None) request.current_company_uuid : 通过 UserRole.CompanyUUID 获取当前角色所属公司UUID + + 注意:此中间件在 DRF 认证之前运行,因此需要自行从 ELT token 解析用户。 """ def __init__(self, get_response): self.get_response = get_response + def _get_user_from_elt(self, request): + """从 ELT token cookie 中解析用户(不依赖 DRF 认证)""" + import uuid as _uuid + import datetime as _datetime + elt_token = request.COOKIES.get(ELT_COOKIE_NAME) + if not elt_token: + return None + try: + token_uuid = _uuid.UUID(elt_token) + except (ValueError, AttributeError): + return None + try: + from gvsdsdk.models import EphemeralToken + elt = EphemeralToken.objects.get(ELTUUID=token_uuid, ELTStatus=1) + except EphemeralToken.DoesNotExist: + return None + now_utc = _datetime.datetime.now(_datetime.timezone.utc) + if elt.ELTEndTime.replace(tzinfo=None) < now_utc.replace(tzinfo=None): + return None + try: + from gvsds.apps.core.models import User + return User.objects.get(UserUUID=elt.ELTUserUUID) + except User.DoesNotExist: + return None + def __call__(self, request): request.current_role = None request.current_company_uuid = None - # 仅对已认证用户处理角色 + # 优先使用已认证的 request.user,否则自行从 ELT token 解析 + user = None if hasattr(request, 'user') and request.user and request.user.is_authenticated: - role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '') - if role_uuid_str: - try: - role_uuid_bytes = UUIDObj(role_uuid_str).bytes - # gvsdsdk.UserRole 的 RoleUUID 是 UUIDField,不能用 select_related - user_role = UserRole.objects.filter( - UserUUID=request.user.UserUUID, - RoleUUID=role_uuid_bytes - ).first() - if user_role: - # 单独查询 Role 实例 - role = Role.objects.filter(RoleUUID=role_uuid_bytes).first() - if role: - request.current_role = role - # 通过 UserRole.CompanyUUID 获取当前公司 - if user_role.CompanyUUID: - request.current_company_uuid = user_role.CompanyUUID - except Exception: - pass + user = request.user + else: + user = self._get_user_from_elt(request) + + if not user: + return self.get_response(request) + + role_uuid_str = request.COOKIES.get('CurrentRoleUUID', '') + logger.info('[CurrentRoleMiddleware] user=%s, CurrentRoleUUID cookie=%s', user, role_uuid_str or '(empty)') + if role_uuid_str: + try: + role_uuid_bytes = UUIDObj(role_uuid_str).bytes + user_uuid = user.UserUUID + logger.info('[CurrentRoleMiddleware] querying UserRole: UserUUID=%s, RoleUUID=%s', + user_uuid.hex() if hasattr(user_uuid, 'hex') else user_uuid, + role_uuid_bytes.hex()) + user_role = UserRole.objects.filter( + UserUUID=user.UserUUID, + RoleUUID=role_uuid_bytes + ).first() + logger.info('[CurrentRoleMiddleware] user_role found=%s', user_role is not None) + if user_role: + role = Role.objects.filter(RoleUUID=role_uuid_bytes).first() + if role: + request.current_role = role + if user_role.CompanyUUID: + request.current_company_uuid = user_role.CompanyUUID + logger.info('[CurrentRoleMiddleware] set current_company_uuid=%s', + user_role.CompanyUUID.hex() if hasattr(user_role.CompanyUUID, 'hex') else user_role.CompanyUUID) + else: + logger.warning('[CurrentRoleMiddleware] user_role.CompanyUUID is None') + except Exception as e: + logger.error('[CurrentRoleMiddleware] error: %s', e, exc_info=True) return self.get_response(request) diff --git a/gvsdsdk/auth/permissions.py b/gvsdsdk/auth/permissions.py index 44543d42..3d4c2584 100644 --- a/gvsdsdk/auth/permissions.py +++ b/gvsdsdk/auth/permissions.py @@ -1,4 +1,5 @@ -from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC +from gvsds.apps.core.models import User +from gvsdsdk.models import UserRole, Role, RolePermission, Permission, UserABAC from rest_framework.permissions import BasePermission @@ -48,7 +49,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None): """权限验证器——混合 ABAC + RBAC 检查 RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限, - 通过 RolePermission → Permission 链路查询。 + 通过 RolePermission → Permission 链路查询,按当前公司过滤。 超级管理员/员工直接放行。 """ if not permission: @@ -63,6 +64,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None): return True current_role = getattr(request, 'current_role', None) if request else None + current_company = getattr(request, 'current_company_uuid', None) if request else None for attr in attributes: # ABAC 检查 @@ -73,13 +75,16 @@ def PermissionVerifier(permission, attributes, user=None, request=None): if abac_match: continue - # RBAC 检查:仅检查当前角色 + # RBAC 检查:仅检查当前角色,按公司过滤权限 if current_role and isinstance(current_role, Role): if current_role.RoleStatus != 1: return False - perm_uuids = RolePermission.objects.filter( + rp_qs = RolePermission.objects.filter( RoleUUID=current_role.RoleUUID - ).values_list('PermUUID', flat=True) + ) + if current_company: + rp_qs = rp_qs.filter(CompanyUUID=current_company) + perm_uuids = rp_qs.values_list('PermUUID', flat=True) rbac_result = Permission.objects.filter( PermUUID__in=perm_uuids, PermCode=attr, @@ -94,9 +99,12 @@ def PermissionVerifier(permission, attributes, user=None, request=None): RoleUUID__in=user_role_uuids, RoleStatus=1, ).values_list('RoleUUID', flat=True) - perm_uuids = RolePermission.objects.filter( + rp_qs = RolePermission.objects.filter( RoleUUID__in=active_role_uuids - ).values_list('PermUUID', flat=True) + ) + if current_company: + rp_qs = rp_qs.filter(CompanyUUID=current_company) + perm_uuids = rp_qs.values_list('PermUUID', flat=True) rbac_result = Permission.objects.filter( PermUUID__in=perm_uuids, PermCode=attr, diff --git a/gvsdsdk/commission/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/commission/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9dcff16ccd8f8b4f95c65978d554c912359824d8 GIT binary patch literal 1232 zcmbtTO=}ZD7@pmv*-c_T8e8#$+7%0BL32@hDg~7kPo*XWp_DG0?2xYRM|pP|jigXS zq@qZnN(J$vC+RN`?7iS6rI)(29{d7t)!d2)-$^!UrJ$e#^XxnGKJ(1XGw;4jBw_^H zhqZGz-h>JH#0Tvr20`N(1}nrMlcY=x;iWJs7~(xLDON*jc1=^yw2!^UbJW{%sC9IesZ6dpTku8 zz?ha?F#N!-w3=qpzzx%T8(ZZ}{vd&ayoI(lZ z^zuF}F>2nXMu(D|Few}ucju|Cn~p)DL+QP4vKa&PTe>w$J3=L)?J0iIW)i~Q_=&JW zYG`SNz!^g{LKd-wc|jno3scQj6KZ5Nc!+tiCNim=S#2}OFh(`8z?H&*QWq+P|4S(n zC`neA9r<@9mTI5ZkZa_*_(EI_ytpf<(J?gw{Bpqk0B#KMn?r;Hzv(yZL>2sq?Hbhb zR1svJineAYTX*oeK$U=3F~m6>;gH~vXT4VKnbd<`&PiTs?~vmqs)qcq zqubOMHQNtWP>Oq^D9WCaoeJcp_W!RbO*0*nXc=%LIESV|KQ7}Qc*_V2WJ`+I zQ{IN;?Z_9llf6sDX9Jtb{>9MN$-YO@!|2k$gVa_kvlwgghBl<39r@y4c)Nt8r^WAt z$kXDEJn|>@cIrA}7oNrOO)281F3CPYTI0m-i=GDg_zE8Cen=nV3RkwnhSU=X8N z5NwYO7!hnE3>dI+EC&oq?6qaEW9QaZW%usgT8-sfGda<%Wp6Yiu~l1<5m{&bV{5;! z`@A$7h~wJ3b*uIi^qkZEb)WBafBn5%|H)=EGjKiBUfZ>@nPL8b58|a0Zsz_TZcZ>P z)6TFeRvlEetH`gqT}^&9?Hc&i1hw6|cAW}gv_XBhq217JY&UkB+D+Z&b~CBh1ufmy zb{o0Z2kqUCc1O3f-PxVfp3|M%p4*+*o=4gY!Tj!m_JZ!h_Cgh-X8fA2IuR-P*I#^5 zMW5PB)XW16YdpxXCV%NgjZj-gYRynxNwI&(Iq4lXxUQHi=Iel&{_1RBT&%d2I`qn=WyaNAH?+v9- zA4$D*{>H%TGoQVfe(wT>oO$QxsiVhe->IvIrmlVXhk*lT^UVIkGatMKZ!^1G4Rx;c z%WtP&`A`Y<$4_F-fzU2rxZ{~WeliG9tLj{-!yn&x{s@JddFNm%b`t7WL+`h10hghGMdo~{0lUXFF8|K0xd=m!9E z7mV=AmGt5LH{LureeF1OgqP2JG?M!G{nXLX>GPLoE?=NHtd$@S49r~l=*ID1Q)B=> zb>VesjsW-u&Tp$n#T%Oa9f59N(4*zGn**URZ+v7|7?AJY#)`OMz0DITt*?2sG|zfVLT+Yjup-0aSj_)UJs!dsOY( zHpZiwhb_nTavkCTI2_*N|G8CAysA3EgyEN7gPJIF1h!LDbx|N$6%$oQ8LvQ=46~Pc zSJet{(1jwq{2XuedILRyu-D7$$h6>WhWx=DyrJ743i&$yyv|Ri%jETT1bv~9*BjEp zyId~Uz&Dk3Pj&7Nu_1QnQymf!Jk{;%fqq=wuDx6VK=u{O6WfB{QYXH$3R*)&Q2cp- znKar44RK@faM72>#mR!wnD&Ty4w@J)4+>a6K(xo#?FU57UT=3V3rhy~RHDP~`uP5w{L;`G+d(WOdZVw#+do6JRkD31TvXaff{>JxD16LV(A$4pxef-GO zr>7}Tz&_%;>NfTE>A~aa!9(lPv7rq3T|O=x=m_k>FT%c|rYGl(hcDmf=6ZvEccaVw zY%jOd&$(-%(v8Sq9qh0UKn}J8)Vp{4f)T8)uUl8&KOeREL+uLs!aI7oZtNru#fkjI zozeAad8B!hEy)d~ppfwA2#X8B8q&qA6J^48oLj;qygU7So7|6VZEo4Ra#j7Rwe@Qo z8dlcVH)PCfxHs$zdVSr!k)Cjqz{P9a4fXYP_4RI#{5d!&q235gp*IqOWu;WLy%(6! z!a)^_6>8cieJ75Am$*EzD6B8^OmD>7uxfP%UIY%-swa$V*BSBkgq{g_*W9%>8(h%u z15!i4zDVCQzMf9+_t*U(12AKy!U6aQlo5NH8s>lHF5li@ua9loClGLe#es4E00ndc z#p2`U-p$IjpSReOP?aRDGy}r=N&+m~0xQ;S)7D4Y+OjN~e^1!YfpGPT#M$J=DeuAU z7L06%6^->SIDUl;=SxJdw-MV!dz>4MbBIIosDU)p0WuqGU#!S$K6y_nb)FBB+ zVC{HMgljJ0^HCKDPq?GkUMh~>Rq^I+gws9t*h9@8HHSisw!R@7m7mc7kJ+(- ziC0IpQBBks)kO`Lv=?!P;OaMp3+Wxhbd`vA@-M54s!uWOZNBjob4Ghn){IPT%o61c zDra06aw)?(R7@D>pIodas)te3;wT4H&oX;7f5SYh@)#nmutW=moI=h@f^KT;>hwEj z)3J|~;8ewyCFTaMGbBr0WKWdMqD*LXQKf(Sm)BBn9CFnx5PdFpx4(NkY|mZ5L7(x3 zPz2T0)lrhivG9^aVxx=d{i&fN)9;K*%mZPRTa$>vMpx?SfvK^VX9k|9rh(8kc4FrI z$h@xDeWA?EDm5iR_gBeGSbwO43lQr@T&MKWqcfk)U#D*0 z9*{1(17W{62y_x`blnh*sr2cA)P;9azdQhz)coPWdhv$Q#39lZR`&V#XZF7`^TEf{ zFTV(@iTrJT2h^cLRC&0V5>hI z;d(MiTIP#eVQg3G@*7jXJ|uN%1zXD|*b>FA>5)d4$HW_P z90`&)wV-~u4YmwzX$5i0I}oN>o>tzuMOOB(EyN)Q1Lc#-imEogwKy4 z+gEC{sgHg|w{xTG8{>Uc95i`sbSOjb_AY-%IK*o@{bBMEytTR4A8PFlx9kD?mD(lf zB;oZP#JJ?OsPXVREZ6Y5?R&%i5U&sRc7k=qYXdzydOc<;ddxCk;7uH$NYq?DXTjFx zoupMkP`opfZg>aUvbzNQ=e5`rLadQ?=|!X-Z=0vG_f^R2Ol^hvD*ZMX zAryyVfSIjh3QJ?=-&sm$)jHc2)nxgyM0s7jye?6`Dqg-SS=BJce%AYO@2rNYSUtz6 zD{_a-x2%k_BH^fxJE{|o+PI^3vY_Nd`?2=pPbCW6@d9_UsO-dZ$DT_Tx{?JI$wjrv z@;baPTaheTHk-#dSI#kJr#Ys*RmhkeM=gVv@xqlOfsrT2jGyQKw(>WXXO8_+%Ab= zyId#Ufe3br_wp|l!Iv}_1&sUkQB4<`;~AGYax?SvlMM3|s3H9ZP)~IK5AfE9QMn>l z)NoM{>|xxO!zF~ z3FqRtb8*616L;23mR6iyescM#+C*t>ytG!qK7nhN*Ueg(9M2r1&oRezvreY0>g>vs zD^GpzCZjjm2Oo}Y8Y)Oy3li4yxV1cCT^hG8%^a_^apGswf0APcQ9ze{8&@h)nLT48>rIgRQs znIuVTi(0yn!DL)foA`|&v%aTgqDIaQxVSUG;8DgUY5_dJB=5IJ?OmQbK_1SB5iKCQ z%#kC&r+g7v;^;W?4wL^t5B^$s7eNLdiY#w^q^}WdW9QZ#BCX1GlZ(-1_TRj z_`~#@SEfIB6JmP!&I_-nFP#Uxr?0-5KJg*E`H!C*p1yRc0cx8X7NS7BhA@|8o~FiL zni@Nle(gmV=;5|US~J0Yk#Mg!M1oi##)q$@hJHTt;xAG!jnL4Xtea#N=@?>rQWh*U z)c3SYFn=zOrapZ&MI8XrJa6>{LZER-&$v|b2qYE+Gw|Y()P*CIzS6PRs4F3T`WIAl zlKO&^jR;(03tDp^=p)DiX*U$@7Y;%IftY{P-jnPMyHfVrJpxZVy32L^gN zA-v|_5e>UzEW+{R={^%o^8Msg>M-HGDW@#58BxaNEh3|G{`ZWS{*Fzm9p zb6LW^Vdekk~dg0bnk?D3FuoU`z%<_1xKTU(ZfGUT5}TC zMRDt*FRc}mxr@h_-aYp4*!uCR2gkQO_GRwl| zV>H#onv;d4!Uq(VB@0TDFFeX+A}yL3W*hsCO4UWL{vxs&0&K$Qq;g|4l-ao!dvhH zL5~9VG!gXHY-$IqyNEmkE>^!2Y#mM$wf372m&$4)C<1zEJX_S(B`JxtAC{;pTrx;d zc2FPeeE`>Q0`)>tB^B`2sEnf{YG;i*n}MuVhaZm`yRxW{Fp8u=Dn>yi&=Jl4T_y-! z3$t(=2%Q4L9<^SwTomR}oDooC-TBj*R7Zr^S7E8(O2Yb|pL zrq7@*069ZcD)OrO)((M`NIxWZTr`ph0wm9LDO(8$5S>KIteTLYn$@&1EasW$T6ryT zjGCnAGBMCOjCj4P;xNAijCB=9(kEy%>l@?xps#y7>$|6~ULnkdkd{(5)U6NpcKCv! zd*FulxCP^dP}~hL!#_S7IT<|Z2#5sHqm6+;Xkn96)3c#*&FLw|}*pEd7iV*UH3~?z2FOiF8ao*0;0{%OW z@ix%j0COGuu0J~VqnJ6l$dy>s5MR_VZY)XW=En?^`C#xL-yAbtFDZ}NCyOhR*7Dyu zH{Q}|O3X3ateMGk-DC<)d9jU2N6yjRgS&@z9Qw(*vM(I9$;C^C>`7(K+7oXjI za`(9%BY}y^b;+{D$*QHZ4#ov;XQM0kPmD1qZ?+t8{;Hm_mJSC-YQLyhI|*3!jCM^F zZOFiK@$w8TE2;$Ce)4&AqI{cx+e>%A&7CM&5eFUM{?UggO72ZoE+;6}%P8F(~kEIc!BQ>)+vv z1j8I+33@XLx;qv7i%>e1dn~y3%OU8J-ZCPAk+fz5S({ZCLBxR zjwR>rA9-lPu{Nen<}FF&x#M~65$ovoiM$`gj7djX!ciG_RG!nGd*+K}n?|>d-al3| zzG~Biqj}ueOs8HMWwyZ3Wa>xZ=DRqnWX+^YIWCNbm*@uQl4K37#}dRvmM{T2{j<5I zz!9W_%mD*9!35_HESI_!;%#PX3(6K3x|o1Xqj_N4BC|hOMTg>%uyIox`m%`((hG(e z39+?_oY~0X4Bgoa%b}6uF=ld3!rqV&xSwJ{Sl;t+7s7cGxgD_zxWiD>SGkZN=CSD& zND5m360#MM6x?x#JK?B{JL)DOy3}{9?|3v(pkjO?r;9m=gP|u)DD02EoR8n_EB`+(K z#W`=DVq(`u``2A9y54%q-1TGFA*n@vqeZ`+ae@c@(!`)5lkM11Ou?;814)f zXQALNq+Z}okVa0BZx(QmAbXOisL|D@UU|B= zea-@!QB;XUL3KG~alBQzZ2Wxri0ZI2bNYGpu29wpzDb(pzS0G}QEFR)ll1~C08xVhAv@%&*1F`tYywcOFhj*X4 zdm_(0SzLKRb8hp5t6`#e6|~J3Fm~{=SnTGQ8eAMSHlMVY410#l&TTyRlksJ1M}1@Z z(LG6f;ZPVu{NsggbQE}QX*4D?sDjSin2CyAE#N3#lE!l#YQ76k1#Y8sJ_zGRRj8HI zE1Nh54u{JPS3hV+Ty9jGH9=1esdzA}6pq#O>!MmP9KkdM_l3R-DK6uZBS^sGbgY&* z26Tr@!K~DwofrAv(1C6;iB<((_vs}_MN7Z;R_c?Zf~`q7p}haTGp|!J1;UHOq%q=N zZ|&{zH>I8*0p6!P2qH5}flR@G*$%;J3Nv-}s*G4x#t;B@AaIj2=OH6WVm$8(E?dF` z^weDoCNA0QsDBch*P~bLSQcxmq2MiB+9Y2oZxA@Zy9TOo;3g&8kGCpO=G8wA@wyJo zapo=qNC-V2!q-t(WWF$vw3CpRWl>ABfjxzr?pYWSHeGSXrV~AX1r?Kdu0&pKJg*k~ zf4GgbxQ)ItgRPRx$vaVUtYoBOsAM8%U2NlZXHHB9X4*|gZ7Ko_%xXXS@ZiHk(J!q_ z!7DfP=-|VHE#TlZ6`>7LAVyvPf8J5oVe-?5el~sWjI^U<()^d-GxXJ&As!_GLX3wT zrucSyhK>ggC*_{uehDARy^qCT*dp8q0L1IJ`Z2|g`w$)$(t93lolovd7YLRtP`m%- zsr{kBhlYY*T9^E_)Se5df&37-HRNCk{2k0I$?qodauuX&OM51p{K)1$21%36c!Lv> zASGmJqRr|AmNE-KhUf+?GM=o|(gv7Db$uWaNiwvIUl!vKlA-Mo9hbgV9;HM5H_%^7 zh1N!H01eQ?f^fKI`r3OCNTm`7f|^rTUrrw#ntpjSb^2=hbB!f!KV5NCfk{Cb^On`~1PII9kv@D~LRjLSwRNy?M@F^K6P~jm+nEP=nSV$@k z6*I)N1N&6g^yJJUbg|M{;%e+>c)%aVOePRS=-!~AGG#s{$s;m~U%<6tCoMjPP#vN2 zg^CxefqZ7{heA>A>*I-Xyh$oItaJxS2( z?mc;Lvec7YR1U(W0EJ6|J=OxrKoCvpAM77~V#2WuJR}xJY&|G=!>f<4AGcOsca#mY zaYxm-vFgva91P?fk(5pJRk;MHFRYd5ta5!roRY8AZm4E{Q(du9ulYNj3d#;C5Choa zgyR3X`{^!(TY6;+@nqGsvTAXlr3n*KizHNFkB>$XUN7|SjjfVV-2OSgFU9mYUq?c`}WM0H@~eycn3|- zOi7ZKGz7fx^uj!#5DBw`#`n^P>4E*3OLY6od*OCb^!$P z8q#OZW&sHXx&vXz`1tIV)TuWFnJ0~rl>s~&@pF5@rd*f-A@$>y0__oD38#~}U%>;0 z6-efQ*GqoT&GoJd&i0b*?ri})&Dt4WPzB-qKFdlO2wa`A4~)f^pi;_p;u{2~*M6)Z zVRk*8J_ve*!ShBYf;Wm&!t2PFb7KI$&g)$}7YR_6_bEZklj-MwX4-HSU0*L&F`6J{KVWuiNO0YG7Mu zHx4zBUI+N&1xBQ{D~mZOCmDh}UVv3F7f=OQJw}+o4Q~N)E=7-7iD3XEFh&*w$n>?d zAjQ*Xha`s%MilF~&}tF&gaxF%7&CSJS-O)WT0 zAemZ)BDLO$fWaDDbZpUu2Sy&8SbEpcA|!M@1geHQ2cP@GQFp;TvgEDWabw*b!Uq^> z`M*ihQ9aQwweZz&Ws@BjX%nPPM2(?*&{DGyB50NvGSEnRDx3;vW{#RV)B&bL{q#B@ zVH4ypqL(b=VzuDW&EBF1GDUL)E|Oa-+}{S;$eKjxd>Uq@5sRQd%~ec$jHfBf@Yltr5ObI`K~q z9D0Lryj?z~|8374!f*=)f&T8w=bPBnrzL#p8s$jvRlZ2^dB(XOeprATbXDkLZJzLWUsjFdq)li^X;*fX|}^ zOa0(7pVvVU01^^5VG}Wfb|b8X8a`RZ;;l-`Q{cK0`oVcPIEg`CM-E&m2#lITr5huG zAWP5kZ9>wO4Whmk^1X02?e@UGLt*gG@J3L4YBCO;iQ<5sGJ~``6FFI?bDF<4GZyDy z^&vN8!kKJe*%@bkGOs9+R~^r*hV+GG>5^pCY7iay%RqGG+hfgONR?FoPo1XNeBGH3 zxrVl!qYn-~IQ01No)PU8&MHRCam{-uKlg?H^%=^I{w(dow)Cb zxb=xy4fKs!XBqebH+a>mn04rhIHc`4^3Xm@7A;N|Le4{VvV0|!fR?iy67(=4sW+Mb z*G(IABLeg{g-sjlwH_^5>>I%Jm{vjtpyT55LkB(u43^i7XKcL|AIvVi|bIl zOH+qvIJXLGv2erx&}&c}WWG|nb;g@pRVv*wn2x41uWPvp-TXb@g)^m#$zI z%*eHk$saV$>6%r#vYVULjk=4d;9BI+sU{s2GZj1g}{3nvWb0dK&=o|tuOVCf$_5>k&qC_pWT(wRf?#G&!ep=r!J z*)I5F6uouqcJV9|hP%0c#9}iR7`> zZ-R5Io5^8Ry1pRIp|WL9ho78Orb)p6i?G0xb&x?u|9;Os-2Z?_*iTI8DJa0~ zSE;^In^g64g^bGjd&cp5#{N%C*?(nfCz#rQVpe>u)~nQiVxaiiu2HEWWyo9>D@vG3 kKsSdzE@9|r-tc>n+a literal 0 HcmV?d00001 diff --git a/gvsdsdk/commission/__pycache__/models.cpython-312.pyc b/gvsdsdk/commission/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1f707e37dc4e11cb5b6af5e75b44966753a3fe3 GIT binary patch literal 18090 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 YWE!c?qaS!*K?r??OPxdl6;XHoA1)B*3jhEB literal 0 HcmV?d00001 diff --git a/gvsdsdk/commission/strategies/__pycache__/engine.cpython-312.pyc b/gvsdsdk/commission/strategies/__pycache__/engine.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..592ba732765319a26ba43fa181d92fd8702d9ab5 GIT binary patch literal 8382 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 ACIA2c literal 0 HcmV?d00001 diff --git a/gvsdsdk/fluent.py b/gvsdsdk/fluent.py index 4b0df1f8..100b3d35 100644 --- a/gvsdsdk/fluent.py +++ b/gvsdsdk/fluent.py @@ -1,146 +1,246 @@ +"""FluentQuery 兼容层查询构建器 + +提供 SQLAlchemy 风格的链式 OOP 查询接口,包装 Django QuerySet。 + +核心类: +- ``FluentQuery`` — 链式查询构建器,由 ``Model.query`` 返回 +- ``Session`` — 简易会话管理(add/commit/rollback) +- ``_CompatFunc`` — 兼容层聚合函数(func.sum / func.count 等) +- ``db`` — 全局数据库操作入口(db.session / db.or_ / db.and_) + +典型用法:: + + from gvsdsdk.fluent import db, func, FQ + + # 链式查询 + orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list() + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # values / annotate + Dingdan.query.values('zhuangtai').annotate(cnt=Count('id')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) + + # 兼容层聚合函数 + func.sum(Dingdan.jine) + func.count(Dingdan.id) + func.year(Dingdan.create_time) +""" +from __future__ import annotations + +import logging import threading -import functools +from typing import Any, Dict, Generic, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Union + from django.db import connection as default_db_connection, models, transaction from django.db.models import Q, F, Count, Sum, Avg, Min, Max, Subquery, OuterRef, Case, When, Value -from gvsdsdk.model_base import FieldExpression +logger = logging.getLogger(__name__) _local = threading.local() +#: 模型类型变量,用于 FluentQuery[_M] 泛型参数 +_M = TypeVar('_M', bound=models.Model) + + +def _get_field_expression_cls(): + """延迟导入 FieldExpression,避免 fluent ↔ model_base 循环依赖。""" + from gvsdsdk.model_base import FieldExpression + return FieldExpression + + +# --------------------------------------------------------------------------- +# 兼容层聚合函数 +# --------------------------------------------------------------------------- class _AggregateExpr: - def __init__(self, model, aggregate): + """聚合表达式包装,关联模型类和 Django Aggregate 对象。""" + + def __init__(self, model: Optional[Type[models.Model]], aggregate: models.Aggregate) -> None: self.model = model self.aggregate = aggregate class _ExtractExpr: - def __init__(self, field_name, lookup_type): + """日期提取表达式(year/month/day),用于 ``func.year(field) == 2024`` 等。""" + + def __init__(self, field_name: str, lookup_type: str) -> None: self.field_name = field_name self.lookup_type = lookup_type - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{f'{self.field_name}__{self.lookup_type}': other}) - def __and__(self, other): + def __and__(self, other) -> Q: if isinstance(other, Q): return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other return self.__eq__(other) class _CompatFunc: - def sum(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Sum(field_expr._django_name) - elif hasattr(field_expr, 'field_name'): - agg = Sum(field_expr.field_name) - else: - agg = Sum(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + """兼容层聚合函数集合,提供 SQLAlchemy 风格的 ``func.sum()`` / ``func.count()`` 等。 + + 用法:: + + from gvsdsdk.fluent import func + + func.sum(Model.jine) → Sum('jine') + func.count(Model.id) → Count('id') + func.max(Model.jine) → Max('jine') + func.min(Model.jine) → Min('jine') + func.avg(Model.jine) → Avg('jine') + func.year(Model.riqi) → _ExtractExpr('riqi', 'year') + func.month(Model.riqi) → _ExtractExpr('riqi', 'month') + func.day(Model.riqi) → _ExtractExpr('riqi', 'day') + """ + + def sum(self, field_expr) -> Union[_AggregateExpr, Sum]: + """求和聚合。""" + name = self._resolve_name(field_expr) + agg = Sum(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def count(self, field_expr=None): + def count(self, field_expr=None) -> Union[_AggregateExpr, Count]: + """计数聚合,无参数时等价于 COUNT(*)。""" if field_expr is None: return Count('*') - if hasattr(field_expr, '_django_name'): - agg = Count(field_expr._django_name) - else: - agg = Count(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + name = self._resolve_name(field_expr) + agg = Count(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def max(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Max(field_expr._django_name) - else: - agg = Max(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def max(self, field_expr) -> Union[_AggregateExpr, Max]: + """最大值聚合。""" + name = self._resolve_name(field_expr) + agg = Max(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def min(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Min(field_expr._django_name) - else: - agg = Min(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def min(self, field_expr) -> Union[_AggregateExpr, Min]: + """最小值聚合。""" + name = self._resolve_name(field_expr) + agg = Min(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def avg(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Avg(field_expr._django_name) - else: - agg = Avg(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def avg(self, field_expr) -> Union[_AggregateExpr, Avg]: + """平均值聚合。""" + name = self._resolve_name(field_expr) + agg = Avg(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def year(self, field_expr): - if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'year') - return _ExtractExpr(str(field_expr), 'year') + def year(self, field_expr) -> _ExtractExpr: + """提取年份,用于 ``func.year(Model.date) == 2024``。""" + return _ExtractExpr(self._resolve_name(field_expr), 'year') - def month(self, field_expr): - if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'month') - return _ExtractExpr(str(field_expr), 'month') + def month(self, field_expr) -> _ExtractExpr: + """提取月份。""" + return _ExtractExpr(self._resolve_name(field_expr), 'month') - def day(self, field_expr): + def day(self, field_expr) -> _ExtractExpr: + """提取日期。""" + return _ExtractExpr(self._resolve_name(field_expr), 'day') + + @staticmethod + def _resolve_name(field_expr) -> str: + """从 FieldExpression 或字符串解析 Django 字段名。""" if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'day') - return _ExtractExpr(str(field_expr), 'day') + return field_expr._django_name + if hasattr(field_expr, 'field_name'): + return field_expr.field_name + return str(field_expr) func = _CompatFunc() +# --------------------------------------------------------------------------- +# DELETE 语句构建器 +# --------------------------------------------------------------------------- + class _DeleteStmt: - def __init__(self, model_class, conditions=None): + """DELETE 语句构建器,支持 ``Model.query.filter(...).delete()`` 风格。""" + + def __init__(self, model_class: Type[models.Model], conditions: Optional[Q] = None) -> None: self.model_class = model_class self.conditions = conditions or Q() - def where(self, *args, **kwargs): + def where(self, *args: Q, **kwargs: Any) -> _DeleteStmt: + """添加 WHERE 条件。""" new_conditions = Q() for a in args: if isinstance(a, Q): new_conditions &= a + if kwargs: + new_conditions &= Q(**kwargs) self.conditions = new_conditions return self - def execute(self): + def execute(self) -> Tuple[int, Dict[str, int]]: + """执行删除,返回 (删除数, {表名: 删除数})。""" if self.model_class and self.conditions: - self.model_class.objects.filter(self.conditions).delete() + return self.model_class.objects.filter(self.conditions).delete() elif self.model_class: - self.model_class.objects.all().delete() + return self.model_class.objects.all().delete() + return (0, {}) -def _get_session(): +# --------------------------------------------------------------------------- +# 会话与缓存 +# --------------------------------------------------------------------------- + +def _get_session() -> Session: + """获取当前线程的 Session 实例。""" if not hasattr(_local, 'db_session'): _local.db_session = Session() return _local.db_session -def _get_cache(): +def _get_cache() -> Dict: + """获取当前线程的查询缓存。""" if not hasattr(_local, 'query_cache'): _local.query_cache = {} return _local.query_cache -def _invalidate_cache(): +def _invalidate_cache() -> None: + """清除当前线程的查询缓存。""" if hasattr(_local, 'query_cache'): _local.query_cache.clear() +# --------------------------------------------------------------------------- +# 关联字段访问器 +# --------------------------------------------------------------------------- + class RelationAccessor: - def __init__(self, model_class, prefix=''): + """关联字段访问器,支持 ``Model.relation.field`` 跨表字段表达式。 + + 用法:: + + # 自动解析关联字段 + Dingdan.query.filter(Dingdan.shangjia.nicheng == '测试') + """ + + def __init__(self, model_class: Type[models.Model], prefix: str = '') -> None: self._model = model_class self._prefix = prefix - def __getattr__(self, name): + def __getattr__(self, name: str): if name.startswith('_'): return object.__getattribute__(self, name) try: self._model._meta.get_field(name) full_name = f'{self._prefix}__{name}' if self._prefix else name - return FieldExpression(self._model, full_name) + return _get_field_expression_cls()(self._model, full_name) except Exception: for rel in self._model._meta.get_fields(include_hidden=True): if hasattr(rel, 'related_model') and rel.related_model: @@ -152,48 +252,64 @@ class RelationAccessor: if rel_name and rel_name == name: full_prefix = f'{self._prefix}__{rel_name}' if self._prefix else rel_name return RelationAccessor(rel.related_model, full_prefix) - return FieldExpression(self._model, name) + return _get_field_expression_cls()(self._model, name) + def __repr__(self) -> str: + return f'RelationAccessor({self._model.__name__}, prefix={self._prefix!r})' + + +# --------------------------------------------------------------------------- +# Values 结果集 +# --------------------------------------------------------------------------- class _ValuesResult: - """ - 包装 Django ValuesQuerySet,支持链式调用。 - 底层使用 Django .values() 返回字典,保持与 Django 行为一致。 - 额外提供 .all() 返回字典列表,.first() 返回字典或 None, - .scalar() 返回单字段场景的标量值。 - """ - def __init__(self, qs, fields, original_fields): - self._qs = qs # Django ValuesQuerySet(返回字典) - self._fields = fields # 转换后的字段名列表(传给 Django .values() 的) - self._original_fields = original_fields # 原始传入的字段(用于判断单字段场景) + """包装 Django ValuesQuerySet,支持链式调用。 - def filter(self, *args, **kwargs): + 底层使用 Django ``.values()`` 返回字典,保持与 Django 行为一致。 + 额外提供 ``.all()`` 返回字典列表,``.first()`` 返回字典或 None, + ``.scalar()`` 返回单字段场景的标量值。 + """ + + def __init__(self, qs: models.QuerySet, fields: List[str], original_fields: Tuple) -> None: + self._qs: models.QuerySet = qs + self._fields: List[str] = fields + self._original_fields: Tuple = original_fields + + @property + def query(self) -> Any: + """暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。""" + return self._qs.query + + def filter(self, *args: Any, **kwargs: Any) -> _ValuesResult: self._qs = self._qs.filter(*args, **kwargs) return self - def exclude(self, *args, **kwargs): + def exclude(self, *args: Any, **kwargs: Any) -> _ValuesResult: self._qs = self._qs.exclude(*args, **kwargs) return self - def order_by(self, *fields): + def order_by(self, *fields: str) -> _ValuesResult: self._qs = self._qs.order_by(*fields) return self - def distinct(self): + def distinct(self) -> _ValuesResult: self._qs = self._qs.distinct() return self - def annotate(self, **kwargs): + def annotate(self, **kwargs: Any) -> _ValuesResult: self._qs = self._qs.annotate(**kwargs) return self - def first(self): + def first(self) -> Optional[Dict[str, Any]]: + """返回第一条记录(字典)或 None。""" return self._qs.first() - def all(self): + def all(self) -> List[Dict[str, Any]]: + """返回所有记录的字典列表。""" return list(self._qs) - def scalar(self): + def scalar(self) -> Any: + """返回单字段场景的标量值。""" row = self._qs.first() if row is None: return None @@ -201,70 +317,97 @@ class _ValuesResult: return row.get(self._fields[0]) return row - def count(self): + def count(self) -> int: return self._qs.count() - def exists(self): + def exists(self) -> bool: return self._qs.exists() - def __getitem__(self, key): + def __getitem__(self, key: Union[int, slice]) -> Any: if isinstance(key, slice): self._qs = self._qs[key] return self else: return self._qs[key] - def __iter__(self): + def __iter__(self) -> Iterator[Dict[str, Any]]: return iter(self._qs) - def __len__(self): + def __len__(self) -> int: return self.count() - def __bool__(self): + def __bool__(self) -> bool: return self._qs.exists() + def __repr__(self) -> str: + return f'_ValuesResult(fields={self._fields}, count={self.count()})' + + +# --------------------------------------------------------------------------- +# FluentQuery 核心查询构建器 +# --------------------------------------------------------------------------- + +class FluentQuery(Generic[_M]): + """QModel 兼容层查询构建器,提供 SQLAlchemy 风格的链式 OOP 查询接口。 + + 由 ``Model.query`` 返回,支持链式调用:: + + # 基础查询 + Dingdan.query.filter(zhuangtai=8).all() + + # 链式过滤 + Dingdan.query.filter(jine__gt=100).order_by('-create_time').limit(10) + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # values / annotate + Dingdan.query.values('zhuangtai').annotate(cnt=Count('id')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) + + 泛型参数 ``_M`` 携带模型类型,使 IDE 能推断链式调用返回的模型实例类型。 + """ + + def __init__(self, model_class: Type[_M]) -> None: + self._model: Type[_M] = model_class + self._qs: models.QuerySet = model_class.objects.all() + self._joined_models: List[Type[models.Model]] = [] + self._cache_enabled: bool = True + @property - def query(self): - """代理内部 QuerySet 的 query 属性,使 Subquery() 可用""" + def query(self) -> Any: + """暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。""" return self._qs.query - def clone(self): - """代理内部 QuerySet 的 clone 方法,使 Subquery() 可用""" - return self._qs.clone() - @property - def model(self): - """代理内部 QuerySet 的 model 属性""" - return self._qs.model + def model(self) -> Type[_M]: + """返回关联的模型类。""" + return self._model - def as_queryset(self): - """返回内部 Django QuerySet,用于 Subquery 等需要原生 QuerySet 的场景""" - return self._qs - - -class FluentQuery: - def __init__(self, model_class): - self._model = model_class - self._qs = model_class.objects.all() - self._joined_models = [] - self._cache_enabled = True - - def _clone(self): + def _clone(self) -> FluentQuery[_M]: + """克隆当前查询状态。""" q = FluentQuery(self._model) q._qs = self._qs q._joined_models = list(self._joined_models) q._cache_enabled = self._cache_enabled return q - def _cache_key(self, method): + def _cache_key(self, method: str) -> Tuple[Any, str, Any]: + """生成缓存键。""" try: query_str = str(self._qs.query) except Exception: query_str = id(self._qs) return (self._model, method, query_str) - def _remap_q(self, q): - new_children = [] + def _remap_q(self, q: Q) -> Q: + """重映射 Q 对象中的字段名(用于 join 跨表查询)。""" + new_children: List[Any] = [] for child in q.children: if isinstance(child, Q): new_children.append(self._remap_q(child)) @@ -281,7 +424,8 @@ class FluentQuery: return new_q return q - def _remap_key(self, key): + def _remap_key(self, key: str) -> str: + """重映射字段名(用于 join 跨表查询自动添加关联前缀)。""" parts = key.split('__') field_name = parts[0] try: @@ -306,8 +450,11 @@ class FluentQuery: pass return key - def filter(self, *args, **kwargs): - converted_args = [] + # ---- 过滤 ---- + + def filter(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]: + """过滤查询,支持 Q 对象和关键字参数,返回 self 以支持链式调用。""" + converted_args: List[Any] = [] for arg in args: if isinstance(arg, Q) and self._joined_models: converted_args.append(self._remap_q(arg)) @@ -316,8 +463,9 @@ class FluentQuery: self._qs = self._qs.filter(*converted_args, **kwargs) return self - def exclude(self, *args, **kwargs): - converted_args = [] + def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]: + """排除查询,返回 self 以支持链式调用。""" + converted_args: List[Any] = [] for arg in args: if isinstance(arg, Q) and self._joined_models: converted_args.append(self._remap_q(arg)) @@ -326,19 +474,25 @@ class FluentQuery: self._qs = self._qs.exclude(*converted_args, **kwargs) return self - def order_by(self, *args): - converted = [] + # ---- 排序 ---- + + def order_by(self, *args: Any) -> FluentQuery[_M]: + """排序,支持字符串和 FieldExpression,返回 self 以支持链式调用。""" + converted: List[str] = [] for a in args: if isinstance(a, str): converted.append(a) - elif isinstance(a, FieldExpression): + elif isinstance(a, _get_field_expression_cls()): converted.append(str(a)) else: converted.append(str(a)) self._qs = self._qs.order_by(*converted) return self - def first(self): + # ---- 获取记录 ---- + + def first(self) -> Optional[_M]: + """返回第一条记录或 None。""" if self._cache_enabled: cache = _get_cache() key = self._cache_key('first') @@ -351,13 +505,26 @@ class FluentQuery: _get_cache()[self._cache_key('first')] = obj return obj - def all(self): + def last(self) -> Optional[_M]: + """返回最后一条记录或 None。""" + return self._qs.last() + + def get(self, *args: Any, **kwargs: Any) -> _M: + """获取唯一匹配记录,不存在或多个时抛异常。""" + if args: + obj = self._qs.filter(*args).get(**kwargs) + else: + obj = self._qs.get(**kwargs) + _get_session().track(obj) + return obj + + def all(self) -> FluentQuery[_M]: """返回 self 以支持链式调用(与 Django QuerySet.all() 一致)。 - 如需获取列表,使用 .to_list() 或 list()。""" + 如需获取列表,使用 ``.to_list()`` 或 ``list()``。""" self._qs = self._qs.all() return self - def to_list(self): + def to_list(self) -> List[_M]: """执行查询并返回模型实例列表。""" if self._cache_enabled: cache = _get_cache() @@ -372,7 +539,8 @@ class FluentQuery: _get_cache()[self._cache_key('to_list')] = results return results - def count(self): + def count(self) -> int: + """返回匹配记录数。""" if self._cache_enabled: cache = _get_cache() key = self._cache_key('count') @@ -383,97 +551,158 @@ class FluentQuery: _get_cache()[self._cache_key('count')] = result return result - def get(self, *args, **kwargs): - if args: - obj = self._qs.filter(*args).get(**kwargs) - else: - obj = self._qs.get(**kwargs) - _get_session().track(obj) - return obj + def exists(self) -> bool: + """判断是否存在匹配记录。""" + return self._qs.exists() - def create(self, **kwargs): + # ---- 创建/更新/删除 ---- + + def create(self, **kwargs: Any) -> _M: + """创建并保存新记录。""" obj = self._model.objects.create(**kwargs) _invalidate_cache() return obj - def get_or_create(self, defaults=None, **kwargs): + def get_or_create(self, defaults: Optional[Dict[str, Any]] = None, + **kwargs: Any) -> Tuple[_M, bool]: + """查询或创建记录,返回 ``(obj, created)`` 元组。""" obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs) if created: _invalidate_cache() _get_session().track(obj) return obj, created - def update_or_create(self, defaults=None, **kwargs): + def update_or_create(self, defaults: Optional[Dict[str, Any]] = None, + **kwargs: Any) -> Tuple[_M, bool]: + """更新或创建记录,返回 ``(obj, created)`` 元组。""" obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs) _invalidate_cache() _get_session().track(obj) return obj, created - def none(self): - self._qs = self._qs.none() - return self - - def last(self): - return self._qs.last() - - def limit(self, n): - self._qs = self._qs[:n] - return self - - def offset(self, n): - self._qs = self._qs[n:] - return self - - def join(self, *args): - for arg in args: - if isinstance(arg, type) and issubclass(arg, models.Model): - if arg not in self._joined_models: - self._joined_models.append(arg) - return self - - def select_related(self, *fields): - self._qs = self._qs.select_related(*fields) - return self - - def prefetch_related(self, *fields): - self._qs = self._qs.prefetch_related(*fields) - return self - - def only(self, *fields): - self._qs = self._qs.only(*fields) - return self - - def defer(self, *fields): - self._qs = self._qs.defer(*fields) - return self - - def select_for_update(self, nowait=False, skip_locked=False, of=(), no_key=False): - self._qs = self._qs.select_for_update( - nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key - ) - return self - - def bulk_create(self, objs, batch_size=None, ignore_conflicts=False): + def bulk_create(self, objs: List[_M], batch_size: Optional[int] = None, + ignore_conflicts: bool = False) -> List[_M]: + """批量创建记录。""" result = self._model.objects.bulk_create( objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts ) _invalidate_cache() return result - def annotate(self, **kwargs): + def bulk_update(self, objs: List[_M], fields: List[str], + batch_size: Optional[int] = None) -> int: + """批量更新记录,返回受影响行数。""" + result = self._model.objects.bulk_update(objs, fields, batch_size=batch_size) + _invalidate_cache() + return result + + def update(self, **kwargs: Any) -> int: + """批量更新匹配记录,返回受影响行数。""" + result = self._qs.update(**kwargs) + _invalidate_cache() + return result + + def delete(self) -> Tuple[int, Dict[str, int]]: + """批量删除匹配记录,返回 ``(删除数, {表名: 删除数})``。""" + result = self._qs.delete() + _invalidate_cache() + return result + + # ---- 查询修饰 ---- + + def none(self) -> FluentQuery[_M]: + """返回空结果集,返回 self 以支持链式调用。""" + self._qs = self._qs.none() + return self + + def limit(self, n: int) -> FluentQuery[_M]: + """限制返回记录数,返回 self 以支持链式调用。""" + self._qs = self._qs[:n] + return self + + def offset(self, n: int) -> FluentQuery[_M]: + """跳过前 n 条记录,返回 self 以支持链式调用。""" + self._qs = self._qs[n:] + return self + + def join(self, *args: Type[models.Model]) -> FluentQuery[_M]: + """关联模型,用于跨表查询自动映射字段,返回 self 以支持链式调用。""" + for arg in args: + if isinstance(arg, type) and issubclass(arg, models.Model): + if arg not in self._joined_models: + self._joined_models.append(arg) + return self + + def select_related(self, *fields: str) -> FluentQuery[_M]: + """外键关联查询优化,返回 self 以支持链式调用。""" + self._qs = self._qs.select_related(*fields) + return self + + def prefetch_related(self, *fields: str) -> FluentQuery[_M]: + """多对多/反向关联查询优化,返回 self 以支持链式调用。""" + self._qs = self._qs.prefetch_related(*fields) + return self + + def only(self, *fields: str) -> FluentQuery[_M]: + """只加载指定字段,返回 self 以支持链式调用。""" + self._qs = self._qs.only(*fields) + return self + + def defer(self, *fields: str) -> FluentQuery[_M]: + """延迟加载指定字段,返回 self 以支持链式调用。""" + self._qs = self._qs.defer(*fields) + return self + + def select_for_update(self, nowait: bool = False, skip_locked: bool = False, + of: Tuple = (), no_key: bool = False) -> FluentQuery[_M]: + """行级锁,返回 self 以支持链式调用。""" + self._qs = self._qs.select_for_update( + nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key + ) + return self + + def distinct(self) -> FluentQuery[_M]: + """去重查询,返回 self 以支持链式调用。""" + self._qs = self._qs.distinct() + return self + + def using(self, alias: str) -> FluentQuery[_M]: + """指定数据库别名,返回 self 以支持链式调用。""" + self._qs = self._qs.using(alias) + return self + + def no_cache(self) -> FluentQuery[_M]: + """禁用查询缓存,返回 self 以支持链式调用。""" + self._cache_enabled = False + return self + + def params(self, **kwargs: Any) -> FluentQuery[_M]: + """参数占位(SQLAlchemy 兼容),返回 self 以支持链式调用。""" + return self + + # ---- 聚合与注解 ---- + + def annotate(self, **kwargs: Any) -> FluentQuery[_M]: + """注解查询,添加聚合/计算字段,返回 self 以支持链式调用。""" self._qs = self._qs.annotate(**kwargs) return self - def aggregate(self, **kwargs): - return self._qs.aggregate(**kwargs) + def aggregate(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + """聚合查询,返回聚合结果字典。 - def values(self, *fields): + 兼容 Django QuerySet.aggregate() 的两种调用方式: + aggregate(Max('field')) # 位置参数 + aggregate(field__max=Max('field')) # 关键字参数 + """ + return self._qs.aggregate(*args, **kwargs) + + def values(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult: + """返回指定字段的字典结果,支持链式调用。""" if not fields: - # 无参数:返回 Django .values()(所有字段字典) - return _ValuesResult(self._qs.values(), [], []) + return _ValuesResult(self._qs.values(), [], fields) - annotations = {} - converted = [] + annotations: Dict[str, models.Aggregate] = {} + converted: List[str] = [] for i, f in enumerate(fields): if isinstance(f, _AggregateExpr): annotations[f'val{i}'] = f.aggregate @@ -489,38 +718,23 @@ class FluentQuery: qs = self._qs if annotations: qs = qs.annotate(**annotations) - # 使用 Django 原生 .values() 返回字典 qs = qs.values(*converted) return _ValuesResult(qs, converted, fields) - def with_entities(self, *fields): + def with_entities(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult: + """values() 的别名,SQLAlchemy 风格。""" return self.values(*fields) - def params(self, **kwargs): - return self - - def values_list(self, *fields, flat=False): + def values_list(self, *fields: str, flat: bool = False) -> Any: + """返回指定字段的元组列表。""" return self._qs.values_list(*fields, flat=flat) - def update(self, **kwargs): - result = self._qs.update(**kwargs) - _invalidate_cache() - return result + # ---- 分页 ---- - def delete(self): - result = self._qs.delete() - _invalidate_cache() - return result - - def exists(self): - return self._qs.exists() - - def distinct(self): - self._qs = self._qs.distinct() - return self - - def paginate(self, page=1, per_page=20, error_out=False): - from django.core.paginator import Paginator + def paginate(self, page: int = 1, per_page: int = 20, + error_out: bool = False) -> Any: + """分页查询,返回 Page 对象(含 items, total, pages 等属性)。""" + from django.core.paginator import Paginator, EmptyPage if not self._qs.ordered: meta = getattr(self._model, '_meta', None) if meta and meta.pk: @@ -528,7 +742,7 @@ class FluentQuery: paginator = Paginator(self._qs, per_page) try: page_obj = paginator.page(page) - except Exception: + except EmptyPage: page_obj = paginator.page(1) page_obj.total = paginator.count page_obj.items = page_obj.object_list @@ -541,55 +755,66 @@ class FluentQuery: page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None return page_obj - def no_cache(self): - self._cache_enabled = False - return self + # ---- 魔术方法 ---- - def using(self, alias): - self._qs = self._qs.using(alias) - return self - - def __getitem__(self, key): - """支持切片 [start:end] 和索引 [n]""" + def __getitem__(self, key: Union[int, slice]) -> Any: + """支持切片 ``[start:end]`` 和索引 ``[n]``。切片返回 self,索引返回模型实例。""" if isinstance(key, slice): self._qs = self._qs[key] return self else: - # 单个索引,返回模型实例 return self._qs[key] - def __iter__(self): + def __iter__(self) -> Iterator[_M]: return iter(self._qs) - def __len__(self): + def __len__(self) -> int: return self.count() - def __bool__(self): + def __bool__(self) -> bool: return self.exists() - def __repr__(self): + def __repr__(self) -> str: return f'FluentQuery({self._model.__name__})' -class Session: - def __init__(self): - self._pending_adds = [] - self._pending_deletes = [] - self._tracked = set() +# --------------------------------------------------------------------------- +# Session 会话管理 +# --------------------------------------------------------------------------- - def add(self, obj): +class Session: + """简易会话管理器,支持 add/delete/commit/rollback 操作。 + + 用法:: + + session = db.session + session.add(obj) + session.commit() + """ + + def __init__(self) -> None: + self._pending_adds: List[models.Model] = [] + self._pending_deletes: List[models.Model] = [] + self._tracked: set = set() + + def add(self, obj: models.Model) -> None: + """添加对象到待保存列表。""" self._pending_adds.append(obj) - def delete(self, obj): + def delete(self, obj: models.Model) -> None: + """添加对象到待删除列表。""" self._pending_deletes.append(obj) self._tracked.discard(obj) - def track(self, obj): + def track(self, obj: models.Model) -> None: + """跟踪对象变更(commit 时自动 save)。""" if isinstance(obj, models.Model): self._tracked.add(obj) - def commit(self): - delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None} + def commit(self) -> None: + """提交所有待处理操作(删除 → 新增 → 更新)。""" + delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes + if obj.pk is not None} with transaction.atomic(): for obj in self._pending_deletes: obj.delete() @@ -601,21 +826,23 @@ class Session: try: obj.save() except Exception: - pass + logger.debug('Session.commit: skip tracked obj %s', obj, exc_info=True) self._pending_adds.clear() self._pending_deletes.clear() self._tracked.clear() _invalidate_cache() - def rollback(self): + def rollback(self) -> None: + """回滚所有待处理操作。""" self._pending_adds.clear() self._pending_deletes.clear() self._tracked.clear() _invalidate_cache() - def query(self, *args): - model = None - aggregates = [] + def query(self, *args) -> Optional[FluentQuery[Any]]: + """创建查询构建器,支持传入模型类或聚合表达式。""" + model: Optional[Type[models.Model]] = None + aggregates: List = [] for a in args: if isinstance(a, type) and issubclass(a, models.Model): model = a @@ -632,10 +859,12 @@ class Session: return fq.values(*args) return None - def flush(self): + def flush(self) -> None: + """flush() 的别名,等同于 commit()。""" self.commit() - def execute(self, stmt): + def execute(self, stmt) -> None: + """执行语句(支持 _DeleteStmt 和原始 SQL 字符串)。""" if isinstance(stmt, _DeleteStmt): stmt.execute() elif isinstance(stmt, str): @@ -644,14 +873,31 @@ class Session: cursor.execute(stmt) +# --------------------------------------------------------------------------- +# 全局入口 +# --------------------------------------------------------------------------- + class DB: + """全局数据库操作入口。 + + 用法:: + + from gvsdsdk.fluent import db + + session = db.session + q = db.or_(Model.field == 1, Model.field == 2) + q = db.and_(Model.field > 0, Model.field < 100) + """ + @property - def session(self): + def session(self) -> Session: + """获取当前线程的 Session 实例。""" return _get_session() @staticmethod - def or_(*args): - result = None + def or_(*args: Q) -> Q: + """逻辑 OR,合并多个 Q 对象。""" + result: Optional[Q] = None for q in args: if isinstance(q, Q): if result is None: @@ -661,7 +907,8 @@ class DB: return result if result is not None else Q() @staticmethod - def and_(*args): + def and_(*args: Q) -> Q: + """逻辑 AND,合并多个 Q 对象。""" result = Q() for q in args: if isinstance(q, Q): @@ -672,5 +919,6 @@ class DB: db = DB() -def FQ(model_class): +def FQ(model_class: Type[_M]) -> FluentQuery[_M]: + """FluentQuery 工厂函数,等同于 ``Model.query``。""" return FluentQuery(model_class) diff --git a/gvsdsdk/management/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/management/__pycache__/__init__.cpython-312.pyc index a66ce6e425031b66ab1b00872adb768732ad8b08..176bffe0e68082e2eb0a494fc37600a77eb0abcf 100644 GIT binary patch delta 12 TcmeBTY-F5Z$E2V)(bEwC6}|&U delta 21 ccmZo<>|&f?$E}%S6%*|1=;!F}I?=%q070(?7ytkO diff --git a/gvsdsdk/management/commands/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/management/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..216c4bc116d193e09b63660635bbc161111d5fa7 GIT binary patch literal 138 zcmX@j%ge<81f}P7vOx4>5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!($tGdFDp(dPRWkR zP0UM7Pt8ruD~UQieuvAGxIV_;^XxSDt~d<y560&y{j@sXL4 Kk+Fyw$N~WA10V+g literal 0 HcmV?d00001 diff --git a/gvsdsdk/management/commands/__pycache__/gvsdsdk_init.cpython-312.pyc b/gvsdsdk/management/commands/__pycache__/gvsdsdk_init.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7365e2b59b92922ed8c7492c3d78b3741e50c8b6 GIT binary patch literal 6976 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 e{-gjA4-g-sB_Eb-6p%^Lv literal 0 HcmV?d00001 diff --git a/gvsdsdk/management/commands/__pycache__/hash_passwords.cpython-312.pyc b/gvsdsdk/management/commands/__pycache__/hash_passwords.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f814d523590866f5402ff9062dc1f3bf291bcf02 GIT binary patch literal 1667 zcma(SOH3O_bauV#wecF<7(*R$aHtj45j6@eEtR4k3Moa3P$iTeEVbLUXW|d*Pj+@g zOpQb&l-Lq#K$VL7iJF{B1Z|?GQc#;J^xTUbknFA$iK^hhDX4BPIdukm3xZ3Zv~S*T z-g`6ez1iQL&K5x6x32z#n*-o4%9y}6iTV&B761ex02#04-j;Ps;=G*!_zqK;ELwy7o!||?+(O^26lv9wftx=>&I`JeW(-0|< zMx+L2noPZe5DNf-5JL{YLMCK^>Mj08`eS zHDNnyb%nuu<(d+5Lc(!W4>>cQ+O9cGeNHf?kRN{)0M(k~p>>kzr})_c5{aJj5bW<^ z0I_)KkeaYih%(DI(=)N+nm)PNFb8rVaqQ@xgy*P?Skl5Cf`s>wfUwY>QBoP(%}S(XxI?Ym58E4WZ2>wQe`cz?xkY1U`-!0>ym|lA z)$rrD-mkhki)ZG=Iic!$c|&wn#1m!lMDfFQ(O(r^H%1C0w>~cU-^h=wi~Uv6J$I>a zzA(J&CP8*PfZch+QE*gj-m=YG^nZK$%hQ$PJ>}y))z;3UFqbX)23Lny-!7dGm&8cj z!gjSh2dv#uCuahq0p74lQc6xDNitd_`iSWYrA0})q{~WE!^M!2QM<{GJ3yr^rJkh7 zY9e~^SR`l$R2}E0hw7Z0cQg#C&1gU{1WAJF2=Uwn6LKn=4n|1Hg8Q`zhRM6C(wEwB z|7VfX)Oa{3>sm}De+Hn^m!T1ryaop{d0{)-$ow6(hbZWDV4Cf&g<+UKz`zsm`V$cR XQ}C<_ozvVu)(*yzAAb(WvX|t);i1Ih literal 0 HcmV?d00001 diff --git a/gvsdsdk/management/commands/hash_passwords.py b/gvsdsdk/management/commands/hash_passwords.py new file mode 100644 index 00000000..b6c1ba3e --- /dev/null +++ b/gvsdsdk/management/commands/hash_passwords.py @@ -0,0 +1,30 @@ +"""将 User 表中明文密码迁移为 bcrypt 哈希""" +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class Command(BaseCommand): + help = '将 User 表中所有明文密码迁移为 bcrypt 哈希' + + def handle(self, *args, **options): + # 筛选非空且非 bcrypt 哈希的密码 + users = User.objects.exclude(UserPassword='') + updated = 0 + skipped = 0 + + for user in users.iterator(): + stored = user.UserPassword or '' + # 已是 bcrypt 哈希则跳过 + if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60: + skipped += 1 + continue + # 明文密码 → bcrypt 哈希 + user.SetPassword(stored) + user.save(update_fields=['UserPassword']) + updated += 1 + + self.stdout.write(self.style.SUCCESS( + f'完成:{updated} 条明文密码已转为 bcrypt,{skipped} 条已是哈希跳过' + )) diff --git a/gvsdsdk/migrations/0001_initial.py b/gvsdsdk/migrations/0001_initial.py index 3a8544f5..f24fe514 100644 --- a/gvsdsdk/migrations/0001_initial.py +++ b/gvsdsdk/migrations/0001_initial.py @@ -1,8 +1,8 @@ -# Generated by Django 6.0.5 on 2026-06-13 11:43 +# Generated by Django 4.2.27 on 2026-06-19 18:42 import datetime -import gvsdsdk.models from django.db import migrations, models +import gvsdsdk.models class Migration(migrations.Migration): @@ -16,9 +16,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='AccountActivation', fields=[ - ('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), - ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False, verbose_name='激活码')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ 'verbose_name': '账户激活', @@ -30,13 +30,13 @@ class Migration(migrations.Migration): migrations.CreateModel( name='EphemeralToken', fields=[ - ('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)), - ('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)), - ('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)), - ('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)), - ('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)), - ('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)), - ('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)), + ('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False, verbose_name='令牌UUID')), + ('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True, verbose_name='用户UUID')), + ('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._EltEndTimeDefault, verbose_name='过期时间')), + ('ELTType', models.SmallIntegerField(db_column='ELTType', default=0, verbose_name='令牌类型')), + ('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list, verbose_name='允许IP')), + ('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1, verbose_name='状态')), ], options={ 'verbose_name': '临时令牌', @@ -48,8 +48,8 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Password', fields=[ - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), - ('Password', models.CharField(db_column='Password', max_length=1024)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')), + ('Password', models.CharField(db_column='Password', max_length=1024, verbose_name='密码哈希')), ], options={ 'verbose_name': '密码', @@ -61,12 +61,14 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Permission', fields=[ - ('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)), - ('PermName', models.CharField(db_column='PermName', max_length=128)), - ('PermCode', models.CharField(db_column='PermCode', max_length=128)), - ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), - ('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)), - ('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)), + ('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False, verbose_name='权限UUID')), + ('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')), + ('PermName', models.CharField(db_column='PermName', max_length=128, verbose_name='权限名称')), + ('PermCode', models.CharField(db_column='PermCode', max_length=128, verbose_name='权限键')), + ('PermCategory', models.CharField(db_column='PermCategory', default='', max_length=64, verbose_name='权限分类')), + ('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1, verbose_name='是否启用')), + ('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512, verbose_name='描述')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ 'verbose_name': '权限', @@ -75,16 +77,72 @@ class Migration(migrations.Migration): 'managed': True, }, ), + migrations.CreateModel( + name='Position', + fields=[ + ('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False, verbose_name='职务UUID')), + ('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')), + ('PositionName', models.CharField(db_column='PositionName', max_length=128, verbose_name='职务名称')), + ('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64, verbose_name='职务编码')), + ('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0, verbose_name='职务级别')), + ('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1, verbose_name='状态')), + ('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512, verbose_name='描述')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '人事职务标签', + 'verbose_name_plural': '人事职务标签', + 'db_table': 'Position', + 'managed': True, + }, + ), + migrations.CreateModel( + name='PositionTemplate', + fields=[ + ('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', primary_key=True, serialize=False, verbose_name='岗位模板UUID')), + ('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')), + ('TemplateName', models.CharField(db_column='TemplateName', max_length=128, verbose_name='模板名称')), + ('TemplateCode', models.CharField(db_column='TemplateCode', default='', max_length=64, verbose_name='模板编码')), + ('TemplateStatus', models.SmallIntegerField(db_column='TemplateStatus', default=1, verbose_name='模板状态')), + ('TemplateDesc', models.CharField(db_column='TemplateDesc', default='', max_length=512, verbose_name='模板描述')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '岗位模板', + 'verbose_name_plural': '岗位模板', + 'db_table': 'PositionTemplate', + 'managed': True, + }, + ), + migrations.CreateModel( + name='PositionTemplatePermission', + fields=[ + ('PTPermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PTPermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')), + ('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')), + ('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '岗位模板权限关联', + 'verbose_name_plural': '岗位模板权限关联', + 'db_table': 'PositionTemplatePermission', + 'managed': True, + }, + ), migrations.CreateModel( name='Role', fields=[ - ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)), - ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')), - ('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)), - ('RoleName', models.CharField(db_column='RoleName', max_length=128)), - ('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)), - ('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)), - ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False, verbose_name='角色UUID')), + ('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')), + ('RoleName', models.CharField(db_column='RoleName', max_length=128, verbose_name='角色名称')), + ('RoleType', models.SmallIntegerField(db_column='RoleType', default=2, verbose_name='角色类型')), + ('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1, verbose_name='角色状态')), + ('AssignScope', models.SmallIntegerField(db_column='AssignScope', default=0, verbose_name='分配可见范围')), + ('AssignScopeUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='AssignScopeUUID', null=True, verbose_name='分配范围UUID')), + ('RoleDesc', models.CharField(db_column='RoleDesc', default='', max_length=512, verbose_name='角色描述')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ 'verbose_name': '角色', @@ -94,55 +152,69 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='RoleDept', + name='RolePermission', fields=[ - ('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)), - ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), - ('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')), - ('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)), + ('RolePermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')), + ('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ - 'verbose_name': '角色部门关联', - 'verbose_name_plural': '角色部门关联', - 'db_table': 'RoleDept', + 'verbose_name': '角色权限关联', + 'verbose_name_plural': '角色权限关联', + 'db_table': 'RolePermission', 'managed': True, }, ), migrations.CreateModel( - name='User', + name='RoleShop', fields=[ - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), - ('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)), - ('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)), - ('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)), - ('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)), - ('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)), - ('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)), - ('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)), - ('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)), - ('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)), - ('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)), - ('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)), - ('IsStaff', models.BooleanField(db_column='IsStaff', default=False)), - ('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)), + ('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False, verbose_name='关联UUID')), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')), + ('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', verbose_name='商铺UUID')), + ('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0, verbose_name='主商铺')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ - 'verbose_name': '用户', - 'verbose_name_plural': '用户', - 'db_table': 'User', + 'verbose_name': '角色商铺关联', + 'verbose_name_plural': '角色商铺关联', + 'db_table': 'RoleShop', + 'managed': True, + }, + ), + migrations.CreateModel( + name='Shop', + fields=[ + ('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False, verbose_name='商铺UUID')), + ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')), + ('ShopName', models.CharField(db_column='ShopName', max_length=128, verbose_name='商铺名称')), + ('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64, verbose_name='商铺编码')), + ('ShopType', models.SmallIntegerField(db_column='ShopType', default=0, verbose_name='商铺类型')), + ('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1, verbose_name='商铺状态')), + ('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512, verbose_name='商铺地址')), + ('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128, verbose_name='联系人')), + ('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32, verbose_name='联系电话')), + ('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512, verbose_name='描述')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '商铺', + 'verbose_name_plural': '商铺', + 'db_table': 'Shop', 'managed': True, }, ), migrations.CreateModel( name='User2FA', fields=[ - ('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)), - ('SecretKey', models.CharField(db_column='SecretKey', max_length=64)), - ('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)), - ('Verified', models.SmallIntegerField(db_column='Verified', default=0)), - ('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)), - ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False, verbose_name='2FA UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')), + ('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')), + ('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0, verbose_name='是否启用')), + ('Verified', models.SmallIntegerField(db_column='Verified', default=0, verbose_name='已验证')), + ('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True, verbose_name='最后使用')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ 'verbose_name': '双因素认证', @@ -154,9 +226,9 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserABAC', fields=[ - ('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), - ('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)), + ('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False, verbose_name='ABAC UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')), + ('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256, verbose_name='层级权限标识')), ], options={ 'verbose_name': '用户ABAC', @@ -168,10 +240,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserAvatar', fields=[ - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)), - ('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)), - ('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)), - ('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')), + ('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True, verbose_name='头像数据')), + ('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow, verbose_name='上传时间')), + ('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16, verbose_name='文件类型')), ], options={ 'verbose_name': '用户头像', @@ -180,16 +252,33 @@ class Migration(migrations.Migration): 'managed': True, }, ), + migrations.CreateModel( + name='UserDept', + fields=[ + ('UserDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserDeptUUID', primary_key=True, serialize=False, verbose_name='关联UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')), + ('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID', verbose_name='部门UUID')), + ('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='PositionUUID', null=True, verbose_name='职务标签UUID')), + ('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1, verbose_name='是否主部门')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), + ], + options={ + 'verbose_name': '员工部门关联', + 'verbose_name_plural': '员工部门关联', + 'db_table': 'UserDept', + 'managed': True, + }, + ), migrations.CreateModel( name='UserLoginLog', fields=[ - ('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)), - ('LoginIP', models.CharField(db_column='LoginIP', max_length=39)), - ('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)), - ('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')), - ('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)), - ('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)), + ('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False, verbose_name='日志UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')), + ('LoginIP', models.CharField(db_column='LoginIP', max_length=39, verbose_name='登录IP')), + ('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32, verbose_name='登录平台')), + ('LoginStatus', models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')), + ('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow, verbose_name='登录时间')), + ('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512, verbose_name='用户代理')), ], options={ 'verbose_name': '登录日志', @@ -201,10 +290,10 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserPending2FA', fields=[ - ('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)), - ('SecretKey', models.CharField(db_column='SecretKey', max_length=64)), - ('ExpireAt', models.DateTimeField(db_column='ExpireAt')), + ('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False, verbose_name='待确认UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True, verbose_name='用户UUID')), + ('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')), + ('ExpireAt', models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')), ], options={ 'verbose_name': '2FA待确认', @@ -216,10 +305,11 @@ class Migration(migrations.Migration): migrations.CreateModel( name='UserRole', fields=[ - ('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)), - ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')), - ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')), - ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)), + ('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False, verbose_name='关联UUID')), + ('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')), + ('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')), + ('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')), + ('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')), ], options={ 'verbose_name': '用户角色关联', diff --git a/gvsdsdk/migrations/0002_alter_user_useruuid.py b/gvsdsdk/migrations/0002_alter_user_useruuid.py new file mode 100644 index 00000000..58f20034 --- /dev/null +++ b/gvsdsdk/migrations/0002_alter_user_useruuid.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.27 on 2026-06-19 01:54 + +from django.db import migrations +import gvsdsdk.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('gvsdsdk', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='UserUUID', + field=gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='用户UUID'), + ), + ] diff --git a/gvsdsdk/migrations/__pycache__/0001_initial.cpython-312.pyc b/gvsdsdk/migrations/__pycache__/0001_initial.cpython-312.pyc index 7fbb292b9135e6e4a187f0118908e30814880126..995e49f72c08a53081bdf2d04e7d921ba2265cef 100644 GIT binary patch literal 15882 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 literal 9719 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 diff --git a/gvsdsdk/migrations/__pycache__/0002_alter_user_useruuid.cpython-312.pyc b/gvsdsdk/migrations/__pycache__/0002_alter_user_useruuid.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b65e9fb06bfb9732a0e043be40e1e73111d8b85 GIT binary patch literal 930 zcmZuv&r2IY6rP#gY<4%XXlf}U+FxGQLr9CKQd)$dDAryolr=DHoH5@nA0s0+g6Df_u0BB! z`l*89lNo^XWdH|=A%+9gz%IrLlYqEd;&oRiXb~}O7crgVdcrs)N&TUgdN%j=qxKY3HFHwr?WB=!v z4%JqD{F;*tGg$V>JT%g?BWpN4bkB8cCnv4v!@(UQv$gGrMQlsvOQq67>W6;Y_X4TM z5f{>21G~1iR6ZL8wwDXB66g2FPrvrQsH|hik*XhhqC*27*4nS7#j13p8N`iHW*Oh~ z;-D>aE#Ws*%@*%S3kZSV-{CU9%|*2taT<@fHsm4>x=s3AHk5&j)QT78Hw6dPpPb5lKsppXA|7W1q>4 zvUAdIw)kMDpV2woKv&Pok1O*lFIE>9D&@Kt)|!=pVwJ!9uYh)ssHd7yC0t?@W3kl{ z>PJkB2_W*o?4rJbteL(>#*KF?eH&TRdv?#9>Y7t0x2I0VCQk|z2gMJ?USYOdnEhJ# zwsX8rd+WTr&X1$FeT2$niGX`W7&$J?!kjlbxyb64-PeqQ@qSs{8TNz~Su7d5$f~I* zHwN<;Q!Nr<#N_aTm{usce;3e|`6QM5sdxY(hBvwormta)FAaq6ouXT(D0gAxv9WLW OMrXRCGnWX0gyRo##P4?i literal 0 HcmV?d00001 diff --git a/gvsdsdk/model_base.py b/gvsdsdk/model_base.py index 078f05d7..360cf16e 100644 --- a/gvsdsdk/model_base.py +++ b/gvsdsdk/model_base.py @@ -1,52 +1,86 @@ +"""QModel 兼容层基类模块 + +提供 SQLAlchemy 风格的链式 OOP 查询接口,使 Django ORM 支持: + +- ``Model.query.filter(...)`` — 链式 FluentQuery 查询 +- ``Model.field_name`` — 类级别返回 FieldExpression(支持 ``==``, ``>``, ``in_()`` 等运算符) +- ``Model.field_name`` — 实例级别返回字段值(与原生 Django Model 一致) +- ``Model[condition]`` — 快捷 filter 语法 + +典型用法:: + + # 链式查询 + orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list() + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1, 2, 3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) +""" +from __future__ import annotations + +import re +from typing import Any, Callable, Dict, Generic, Optional, Tuple, Type, TypeVar, Union + +try: + from typing import Self +except ImportError: + from typing_extensions import Self + from django.db import models from django.db.models import Q +from .fluent import FluentQuery -class classproperty: - def __init__(self, func): + +# --------------------------------------------------------------------------- +# 工具函数 +# --------------------------------------------------------------------------- + +#: classproperty 返回值类型变量 +_RT = TypeVar('_RT') + +#: QModel 子类类型变量(仅用于 __class_getitem__ 兼容) +_QM = TypeVar('_QM', bound='QModel') + + +class classproperty(Generic[_RT]): + """类属性描述符,支持 ``Model.query`` 等类级别属性访问。 + + 泛型参数 ``_RT`` 传播函数返回类型,使 Pylance 能穿透描述符推断 + ``Model.query`` 的返回类型为 ``FluentQuery[Self]``。 + """ + + def __init__(self, func: Callable[[type], _RT]) -> None: self.fget = func - def __get__(self, obj, objtype=None): + def __get__(self, obj: Any, objtype: Optional[type] = None) -> _RT: return self.fget(objtype if objtype is not None else type(obj)) -ABBREVIATIONS = { - 'uuid': 'UUID', - 'id': 'ID', - 'ip': 'IP', - 'url': 'URL', - 'uri': 'URI', - 'api': 'API', - 'html': 'HTML', - 'css': 'CSS', - 'js': 'JS', - 'json': 'JSON', - 'xml': 'XML', - 'sql': 'SQL', - 'http': 'HTTP', - 'https': 'HTTPS', - 'smtp': 'SMTP', - 'ssl': 'SSL', - 'tls': 'TLS', - 'ssh': 'SSH', - 'ftp': 'FTP', - '2fa': '2FA', - 'elt': 'ELT', - 'sve': 'SVE', - 't4': 'T4', - 'mc': 'MC', - 'crb': 'CRB', - 'ccgo': 'CCGO', - 'ugm': 'UGM', - 'gerp': 'GERP', - 'abac': 'ABAC', - 'rbac': 'RBAC', - 'smtm': 'SMTM', - 'pwd': 'PWD', +ABBREVIATIONS: Dict[str, str] = { + 'uuid': 'UUID', 'id': 'ID', 'ip': 'IP', 'url': 'URL', 'uri': 'URI', + 'api': 'API', 'html': 'HTML', 'css': 'CSS', 'js': 'JS', 'json': 'JSON', + 'xml': 'XML', 'sql': 'SQL', 'http': 'HTTP', 'https': 'HTTPS', + 'smtp': 'SMTP', 'ssl': 'SSL', 'tls': 'TLS', 'ssh': 'SSH', 'ftp': 'FTP', + '2fa': '2FA', 'elt': 'ELT', 'sve': 'SVE', 't4': 'T4', 'mc': 'MC', + 'crb': 'CRB', 'ccgo': 'CCGO', 'ugm': 'UGM', 'gerp': 'GERP', + 'abac': 'ABAC', 'rbac': 'RBAC', 'smtm': 'SMTM', 'pwd': 'PWD', } -def snake_to_pascal(name): +def snake_to_pascal(name: str) -> str: + """将 snake_case 转换为 PascalCase,自动识别常见缩写。 + + >>> snake_to_pascal('user_uuid') + 'UserUUID' + >>> snake_to_pascal('paifa_dingdan_shu') + 'PaifaDingdanShu' + """ if '_' not in name: return name[0].upper() + name[1:] if name else name parts = name.split('_') @@ -60,46 +94,74 @@ def snake_to_pascal(name): return ''.join(result) -def pascal_to_snake(name): - import re +def pascal_to_snake(name: str) -> str: + """将 PascalCase 转换为 snake_case。 + + >>> pascal_to_snake('UserUUID') + 'user_uuid' + """ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) - parts = s2.lower().split('_') - return '_'.join(parts) + return s2.lower() +# --------------------------------------------------------------------------- +# 字段表达式(类级别访问 Model.field 时返回) +# --------------------------------------------------------------------------- + class _InvertedFieldExpression: - def __init__(self, field_expr): + """取反字段表达式,由 ``~Model.field`` 产生,支持 ``~field.startswith(...)`` 等否定查询。""" + + def __init__(self, field_expr: FieldExpression) -> None: self._field_expr = field_expr - def startswith(self, value): + def startswith(self, value: str) -> Q: return ~Q(**{f'{self._field_expr._django_name}__startswith': value}) - def contains(self, value): + def contains(self, value: str) -> Q: return ~Q(**{f'{self._field_expr._django_name}__contains': value}) - def like(self, pattern): + def like(self, pattern: str) -> Q: clean = pattern.strip('%') return ~Q(**{f'{self._field_expr._django_name}__icontains': clean}) - def in_(self, values): + def in_(self, values) -> Q: return ~Q(**{f'{self._field_expr._django_name}__in': values}) - def is_(self, other): + def is_(self, other) -> Q: if other is None: return ~Q(**{f'{self._field_expr._django_name}__isnull': True}) return ~Q(**{self._field_expr._django_name: other}) + def between(self, low, high) -> Q: + return ~(Q(**{f'{self._field_expr._django_name}__gte': low}) + & Q(**{f'{self._field_expr._django_name}__lte': high})) + + def __repr__(self) -> str: + return f'~{self._field_expr!r}' + class FieldExpression: - _resolve_cache = {} + """字段表达式,由类级别 ``Model.field_name`` 访问产生。 - def __init__(self, model_class, field_name): + 支持通过 Python 运算符构建 Django Q 对象:: + + Model.field == value → Q(field=value) + Model.field > value → Q(field__gt=value) + Model.field.in_([1,2,3]) → Q(field__in=[1,2,3]) + Model.field.between(a, b) → Q(field__gte=a) & Q(field__lte=b) + ~Model.field → _InvertedFieldExpression(否定查询) + """ + + _resolve_cache: Dict[Tuple[str, str], str] = {} + + def __init__(self, model_class: Type, field_name: str) -> None: self.model_class = model_class self.field_name = field_name - self._django_name = self._resolve_django_name() + self._django_name: str = self._resolve_django_name() - def _resolve_django_name(self): + def _resolve_django_name(self) -> str: + """将 Python 字段名解析为 Django ORM 查询使用的字段名。""" cache_key = (self.model_class.__name__, self.field_name) if cache_key in FieldExpression._resolve_cache: return FieldExpression._resolve_cache[cache_key] @@ -111,221 +173,232 @@ class FieldExpression: FieldExpression._resolve_cache[cache_key] = self.field_name return self.field_name - def __eq__(self, other): + # ---- 比较运算符 ---- + + def __eq__(self, other) -> Q: # type: ignore[override] if isinstance(other, FieldExpression): return NotImplemented if other is None: return Q(**{f'{self._django_name}__isnull': True}) return Q(**{self._django_name: other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] if isinstance(other, FieldExpression): return NotImplemented return ~Q(**{self._django_name: other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{f'{self._django_name}__gt': other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{f'{self._django_name}__gte': other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{f'{self._django_name}__lt': other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{f'{self._django_name}__lte': other}) - def __getitem__(self, key): - return _JsonKeyExpression(self._django_name, key) - - def __hash__(self): + def __hash__(self) -> int: return hash((self.model_class.__name__, self.field_name)) - def __and__(self, other): + # ---- 逻辑运算符 ---- + + def __and__(self, other) -> Q: if isinstance(other, Q): return Q(self) & other + if isinstance(other, FieldExpression): + return Q(self) & Q(other) return NotImplemented - def __or__(self, other): + def __or__(self, other) -> Q: if isinstance(other, Q): return Q(self) | other + if isinstance(other, FieldExpression): + return Q(self) | Q(other) return NotImplemented - def __invert__(self): + def __invert__(self) -> _InvertedFieldExpression: return _InvertedFieldExpression(self) - def is_(self, other): + # ---- 查询方法 ---- + + def is_(self, other) -> Q: + """``field IS NULL`` 或 ``field IS value``。""" if other is None: return Q(**{f'{self._django_name}__isnull': True}) return Q(**{self._django_name: other}) - def is_not(self, other): + def is_not(self, other) -> Q: + """``field IS NOT NULL`` 或 ``field IS NOT value``。""" if other is None: return Q(**{f'{self._django_name}__isnull': False}) return ~Q(**{self._django_name: other}) - def like(self, pattern): + def like(self, pattern: str) -> Q: + """模糊匹配(不区分大小写),自动去除 ``%`` 通配符。""" clean = pattern.strip('%') return Q(**{f'{self._django_name}__icontains': clean}) - def contains(self, value): + def contains(self, value: str) -> Q: + """包含子串(区分大小写)。""" return Q(**{f'{self._django_name}__contains': value}) - def ilike(self, pattern): + def ilike(self, pattern: str) -> Q: + """like() 的别名。""" return self.like(pattern) - def startswith(self, value): + def startswith(self, value: str) -> Q: + """前缀匹配(区分大小写)。""" return Q(**{f'{self._django_name}__startswith': value}) - def istartswith(self, value): + def istartswith(self, value: str) -> Q: + """前缀匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__istartswith': value}) - def endswith(self, value): + def endswith(self, value: str) -> Q: + """后缀匹配(区分大小写)。""" return Q(**{f'{self._django_name}__endswith': value}) - def iendswith(self, value): + def iendswith(self, value: str) -> Q: + """后缀匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__iendswith': value}) - def in_(self, values): + def in_(self, values) -> Q: + """``field IN (values)``,支持列表和 FluentQuery 子查询。""" if hasattr(values, '_qs'): return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)}) return Q(**{f'{self._django_name}__in': values}) - def between(self, low, high): + def between(self, low, high) -> Q: + """``field BETWEEN low AND high``。""" return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high}) - def regex(self, pattern): + def regex(self, pattern: str) -> Q: + """正则匹配(区分大小写)。""" return Q(**{f'{self._django_name}__regex': pattern}) - def iregex(self, pattern): + def iregex(self, pattern: str) -> Q: + """正则匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__iregex': pattern}) - def desc(self): + # ---- 排序辅助 ---- + + def desc(self) -> str: + """返回降序排序字符串 ``'-field_name'``。""" return f'-{self._django_name}' - def asc(self): + def asc(self) -> str: + """返回升序排序字符串 ``'field_name'``。""" return self._django_name - def __repr__(self): + # ---- JSON 字段访问 ---- + + def __getitem__(self, key: str) -> _JsonKeyExpression: + """访问 JSON 字段内的键,如 ``Model.meta['key'] == value``。""" + return _JsonKeyExpression(self._django_name, key) + + # ---- 显示 ---- + + def __repr__(self) -> str: return f'FieldExpr({self.model_class.__name__}.{self.field_name})' - def __str__(self): + def __str__(self) -> str: return self._django_name class _JsonKeyExpression: - def __init__(self, field_name, key): + """JSON 字段键表达式,由 ``Model.json_field['key']`` 产生。""" + + def __init__(self, field_name: str, key: str) -> None: self.field_name = field_name self.key = key - def _lookup(self, suffix=''): + def _lookup(self, suffix: str = '') -> str: if suffix: return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}' - def cast(self, type_hint): + def cast(self, type_hint) -> _CastJsonExpression: + """将 JSON 值转换为指定类型后再比较。""" return _CastJsonExpression(self.field_name, self.key, type_hint) - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{self._lookup(): other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] return ~Q(**{self._lookup(): other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{self._lookup('gt'): other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{self._lookup('gte'): other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{self._lookup('lt'): other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{self._lookup('lte'): other}) - def between(self, low, high): + def between(self, low, high) -> Q: return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) - def __repr__(self): + def __repr__(self) -> str: return f'JsonKeyExpr({self.field_name}[{self.key!r}])' class _CastJsonExpression: - def __init__(self, field_name, key, type_hint): + """类型转换后的 JSON 键表达式。""" + + def __init__(self, field_name: str, key: str, type_hint) -> None: self.field_name = field_name self.key = key self.type_hint = type_hint - def _lookup(self, suffix=''): + def _lookup(self, suffix: str = '') -> str: if suffix: return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}' - def between(self, low, high): - return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) - - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{self._lookup(): other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] return ~Q(**{self._lookup(): other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{self._lookup('gt'): other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{self._lookup('gte'): other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{self._lookup('lt'): other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{self._lookup('lte'): other}) - def __repr__(self): + def between(self, low, high) -> Q: + return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) + + def __repr__(self) -> str: type_name = getattr(self.type_hint, '__name__', str(self.type_hint)) return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})' -class ForeignKeyIdDescriptor: - def __init__(self, fk_name): - self.fk_name = fk_name - self.id_attr = fk_name + '_id' - - def __get__(self, obj, objtype=None): - if obj is None: - return FieldExpression(objtype, self.id_attr) - return getattr(obj, self.id_attr) - - def __set__(self, obj, value): - setattr(obj, self.id_attr, value) - - -class FieldExpressionDescriptor: - def __init__(self, field_name): - self.field_name = field_name - - def __get__(self, obj, objtype=None): - if obj is None: - return FieldExpression(objtype, self.field_name) - return getattr(obj, self.field_name) - - def __set__(self, obj, value): - setattr(obj, self.field_name, value) - +# --------------------------------------------------------------------------- +# QModel 元类与基类 +# --------------------------------------------------------------------------- class QModelBase(models.base.ModelBase): - def __new__(mcs, name, bases, namespace, **kwargs): - from django.db import models as dj_models - for attr_name, attr_val in list(namespace.items()): - if isinstance(attr_val, dj_models.Field) and not attr_val.db_column: - # ForeignKey/OneToOneField 的数据库列名是 attr_name + '_id' - if isinstance(attr_val, (dj_models.ForeignKey, dj_models.OneToOneField)): - attr_val.db_column = attr_name + '_id' - else: - attr_val.db_column = attr_name + """QModel 元类,负责: + 1. 根据 ``GVSDSDK_MANAGE_TABLES`` 设置动态控制 ``managed`` + 2. 包装字段描述符,使类级别访问返回 FieldExpression + """ + + def __new__(mcs, name, bases, namespace, **kwargs): # 动态设置 managed:子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表 meta = namespace.get('Meta') if meta and getattr(meta, 'app_label', None) == 'gvsdsdk': @@ -336,16 +409,132 @@ class QModelBase(models.base.ModelBase): except Exception: pass - return super().__new__(mcs, name, bases, namespace, **kwargs) + cls = super().__new__(mcs, name, bases, namespace, **kwargs) + + # 模型创建后,包装字段描述符以支持类级别 FieldExpression + if not cls._meta.abstract: + _wrap_field_descriptors(cls) + + return cls + + +def _wrap_field_descriptors(cls: Type) -> None: + """包装 Django 字段描述符,使类级别访问返回 FieldExpression, + 实例级别完全委托给 Django 原生描述符。 + + Django 的字段描述符(DeferredAttribute / ForwardManyToOneDescriptor): + - ``__get__(None, cls)`` → 返回字段对象本身(类级别) + - ``__get__(instance, cls)`` → 返回字段值(实例级别) + + 我们包装后: + - ``__get__(None, cls)`` → 返回 FieldExpression(类级别,用于链式 OOP) + - ``__get__(instance, cls)`` → 委托给原生描述符(实例级别,正常行为) + """ + from django.db.models.query_utils import DeferredAttribute + from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ) + + for field in cls._meta.local_fields: + fname = field.name + if fname.startswith('_'): + continue + + original_desc = cls.__dict__.get(fname) + if original_desc is None: + continue + + # 只包装已知的 Django 描述符类型 + if isinstance(original_desc, (DeferredAttribute, ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor)): + wrapper = _FieldExpressionWrapper(original_desc, cls, fname) + setattr(cls, fname, wrapper) + + +class _FieldExpressionWrapper: + """包装 Django 字段描述符,类级别返回 FieldExpression,实例级别委托给原生描述符。 + + 这是 QModel 字段表达式机制的核心:当你在类上访问 ``Model.field_name`` 时, + 返回 ``FieldExpression`` 对象,支持 ``Model.field == value`` 等运算符构建 Q 对象; + 当你在实例上访问 ``obj.field_name`` 时,行为与原生 Django 完全一致。 + """ + + def __init__(self, original_descriptor, model_class: Type, field_name: str) -> None: + self._original = original_descriptor + self._model = model_class + self._field_name = field_name + self._has_set: bool = hasattr(original_descriptor, '__set__') + + def __get__(self, obj, objtype=None): + if obj is None: + # 类级别访问:返回 FieldExpression + return FieldExpression(self._model, self._field_name) + # 实例级别访问:委托给原生 Django 描述符 + return self._original.__get__(obj, objtype) + + def __set__(self, obj, value) -> None: + if self._has_set: + # FK/O2O 描述符有 __set__ + self._original.__set__(obj, value) + else: + # 普通字段(DeferredAttribute)没有 __set__,直接写 __dict__ + obj.__dict__[self._field_name] = value + + def __repr__(self) -> str: + return f'_FieldExpressionWrapper({self._model.__name__}.{self._field_name})' class QModel(models.Model, metaclass=QModelBase): + """QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。 + + 所有继承 QModel 的模型自动获得: + + - ``Model.query`` : 返回 FluentQuery,支持链式查询 + - ``Model.field_name`` : 类级别访问返回 FieldExpression,支持表达式查询 + - ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致) + - ``Model[condition]`` : 快捷 filter 语法 + + 用法示例:: + + # 链式查询 + orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list() + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # values / annotate + Dingdan.query.values('zhuangtai').annotate(cnt=Count('id')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) + + # 快捷 filter + Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list() + """ + class Meta: abstract = True @classproperty - def query(cls): - from .fluent import FluentQuery + def query(cls) -> FluentQuery[Self]: + """返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。""" return FluentQuery(cls) + @classmethod + def __class_getitem__(cls, item) -> FluentQuery[Self]: + """支持 ``Model[condition]`` 语法,返回 filter 后的 FluentQuery。 + 用法:: + + Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list() + Dingdan[{'zhuangtai': 8}].to_list() + """ + if isinstance(item, Q): + return cls.query.filter(item) + if isinstance(item, dict): + return cls.query.filter(**item) + return cls.query.filter(item) diff --git a/gvsdsdk/models.py b/gvsdsdk/models.py index 310a117f..20262f94 100644 --- a/gvsdsdk/models.py +++ b/gvsdsdk/models.py @@ -1,813 +1,826 @@ -"""GVS-DSDK 数据模型模块 - -将 SAAS 主服务器的用户表(User)、权限表(Permission)、角色表(Role)、 -用户角色关联表(UserRole)、角色权限关联表(RolePermission)、 -岗位模板表(PositionTemplate)、岗位模板权限关联表(PositionTemplatePermission)、 -人事职务标签表(Position)、员工部门关联表(UserDept)封装为独立的 Django ORM 模型, -供子服务器直接进行跨服务器 ORM 查询。 - -权限体系四大分层: - 1. 组织人事层:Tenant → Company → Department,员工通过 UserDept 归属部门 - 2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照 - 3. 角色权限载体层:Role + RolePermission,运行时鉴权实体 - 4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性) - -使用方式(子服务器): - from gvsdsdk.models import User, Permission, Role, UserRole, RolePermission - - # 查询所有启用的用户 - users = User.objects.using('saas').filter(UserAccountLicense=1) - - # 或配置 DatabaseRouter 后直接使用 - users = User.objects.filter(UserAccountLicense=1) - -设计原则: - 1. 独立:不依赖 gvsds 内部模块,可在任意 Django 项目中独立使用 - 2. 非托管:managed=False,不创建/迁移表,仅映射已有表结构 - 3. PascalCase:字段名、方法名遵循帕斯卡命名法 - 4. 兼容:db_table、db_column 与 SAAS 主库完全一致 - 5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见 -""" - -import uuid as _uuid -import datetime as _datetime -from django.db import models -from django.contrib.auth.models import BaseUserManager -from gvsdsdk.model_base import QModel -from gvsdsdk.model_utils import UUIDObj - - -# === UUID 字段 === - -class FixedBinaryField(models.BinaryField): - """定长二进制字段:映射 MySQL BINARY(N),可用于主键""" - - description = "Fixed-length binary field" - - def __init__(self, *args, **kwargs): - self.binary_length = kwargs.pop('binary_length', 16) - kwargs.setdefault('max_length', self.binary_length) - kwargs.setdefault('editable', False) - super().__init__(*args, **kwargs) - - def db_type(self, connection): - if connection.vendor == 'mysql': - return f'BINARY({self.binary_length})' - elif connection.vendor == 'sqlite': - return 'BLOB' - elif connection.vendor == 'postgresql': - return 'BYTEA' - return f'BINARY({self.binary_length})' - - def rel_db_type(self, connection): - return self.db_type(connection) - - def deconstruct(self): - name, path, args, kwargs = super().deconstruct() - kwargs['binary_length'] = self.binary_length - return name, path, args, kwargs - - -class UUIDField(FixedBinaryField): - """UUID 字段:数据库存储为 BINARY(16),Python 侧自动转换 str/bytes/uuid.UUID""" - - description = "UUID stored as BINARY(16)" - - def __init__(self, *args, **kwargs): - kwargs.setdefault('max_length', 16) - kwargs.setdefault('editable', False) - super().__init__(*args, **kwargs) - - def db_type(self, connection): - vendor = connection.vendor - if vendor == 'mysql': - return 'BINARY(16)' - elif vendor == 'sqlite': - return 'BLOB' - elif vendor == 'postgresql': - return 'BYTEA' - return 'BINARY(16)' - - def from_db_value(self, value, expression, connection): - if value is None: - return None - if isinstance(value, UUIDObj): - return value - return UUIDObj(value) - - def get_prep_value(self, value): - if value is None: - return None - if isinstance(value, UUIDObj): - return value.bytes - if isinstance(value, str): - try: - return _uuid.UUID(value).bytes - except ValueError: - return None - if isinstance(value, _uuid.UUID): - return value.bytes - if isinstance(value, bytes): - return value - return None - - def get_prep_lookup(self, lookup_type, value): - if lookup_type in ('exact', 'iexact', 'in'): - if value is None: - return None - if lookup_type == 'in' and isinstance(value, (list, tuple)): - return [self.get_prep_value(v) for v in value] - return self.get_prep_value(value) - return super().get_prep_lookup(lookup_type, value) - - def value_to_string(self, obj): - value = self.value_from_object(obj) - if isinstance(value, UUIDObj): - return value.str or '' - if isinstance(value, str): - return value - if isinstance(value, bytes) and len(value) == 16: - return str(_uuid.UUID(bytes=value)) - if isinstance(value, _uuid.UUID): - return str(value) - return '' - - def deconstruct(self): - name, path, args, kwargs = super().deconstruct() - del kwargs['max_length'] - return name, path, args, kwargs - - -# === QModel 基础模型 === - -# === 用户管理器 === - -class UserManager(BaseUserManager): - """用户管理器——提供用户创建与超级用户创建。 - 方法名保留蛇形命名以兼容 Django 内部约定。""" - - def create_user(self, username, password=None, email='', **extra_fields): - """创建普通用户""" - if not username: - raise ValueError('The Username field must be set') - user = self.model( - UserUUID=_uuid.uuid4().bytes, - UserName=username, - UserEmail=email or '', - **extra_fields - ) - if password: - user.SetPassword(password) - user.save(using=self._db) - return user - - def create_superuser(self, username, password=None, **extra_fields): - """创建超级用户""" - extra_fields.setdefault('UserAccountLicense', 1) - extra_fields.setdefault('UserPositionStatus', 0) - extra_fields.setdefault('IsStaff', True) - extra_fields.setdefault('IsSuperuser', True) - return self.create_user(username, password, **extra_fields) - - -# === 用户表 === -class User(QModel): - """用户表——系统核心身份模型(映射 SAAS 主库 User 表)""" - - UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') - UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名') - UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码') - UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态') - UserPersonalSignature = models.CharField(max_length=1024, default='', db_column='UserPersonalSignature', verbose_name='个人签名') - UserEmail = models.CharField(max_length=256, default='', db_column='UserEmail', verbose_name='邮箱') - UserEmailVerify = models.SmallIntegerField(default=0, db_column='UserEmailVerify', verbose_name='邮箱验证') - UserAccountLicense = models.SmallIntegerField(default=1, db_column='UserAccountLicense', verbose_name='账户许可') - UserGender = models.SmallIntegerField(default=-1, db_column='UserGender', verbose_name='性别') - UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话') - UserCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserCreateTime', verbose_name='创建时间') - UserLastLoginDate = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserLastLoginDate', verbose_name='最后登录') - IsStaff = models.BooleanField(default=False, db_column='IsStaff', verbose_name='是否员工') - IsSuperuser = models.BooleanField(default=False, db_column='IsSuperuser', verbose_name='是否超级用户') - - objects = UserManager() - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'User' - verbose_name = '用户' - verbose_name_plural = '用户' - - def __str__(self): - return self.UserName - - @property - def UUIDStr(self): - """返回 UUID 字符串形式""" - return UUIDObj(self.UserUUID).str - - @property - def IsActive(self): - """账户是否启用(UserAccountLicense == 1)""" - return self.UserAccountLicense == 1 - - @property - def IsOnJob(self): - """是否在职(UserPositionStatus == 0)""" - return self.UserPositionStatus == 0 - - @property - def IsAnonymous(self): - return False - - @property - def IsAuthenticated(self): - return True - - @property - def PK(self): - return UUIDObj(self.UserUUID) - - @property - def ID(self): - return UUIDObj(self.UserUUID) - - def SetPassword(self, RawPassword): - """设置密码——bcrypt 哈希""" - import bcrypt - salt = bcrypt.gensalt(rounds=12) - hashed = bcrypt.hashpw(RawPassword.encode('utf-8'), salt) - self.UserPassword = hashed.decode('utf-8') - - def CheckPassword(self, RawPassword): - """验证密码""" - import bcrypt - try: - return bcrypt.checkpw( - RawPassword.encode('utf-8'), - self.UserPassword.encode('utf-8') - ) - except Exception: - return False - - def GetTele(self): - """获取电话,空则返回默认文本""" - return self.UserTele or '未填写' - - def GetCompanies(self): - """获取用户所属公司列表""" - # Company 模型不在 gvsdsdk 中,此处返回空 QuerySet - # 如需使用,请在子服务器中自行实现 - return [] - - def GetRoles(self): - """获取用户角色列表(需在同一 database 连接下使用)""" - user_role_uuids = UserRole.objects.filter( - UserUUID=self.UserUUID - ).values_list('RoleUUID', flat=True) - return Role.objects.filter(RoleUUID__in=user_role_uuids) - - def GetDepartments(self): - """获取用户部门列表""" - # Department 模型不在 gvsdsdk 中,此处返回空列表 - # 如需使用,请在子服务器中自行实现 - return [] - - def HasPermission(self, PermCode): - """检查用户是否拥有指定权限(需在同一 database 连接下使用) - - 通过 UserRole → RolePermission → Permission 链路查询, - 汇总所有角色的操作权限后判断。 - """ - user_role_uuids = UserRole.objects.filter( - UserUUID=self.UserUUID - ).values_list('RoleUUID', flat=True) - perm_uuids = RolePermission.objects.filter( - RoleUUID__in=user_role_uuids - ).values_list('PermUUID', flat=True) - return Permission.objects.filter( - PermUUID__in=perm_uuids, - PermCode=PermCode, - PermStatus=1, - ).exists() - - # Django auth 框架兼容(内部使用蛇形命名) - @property - def is_active(self): - return self.IsActive - - @property - def is_anonymous(self): - return self.IsAnonymous - - @property - def is_authenticated(self): - return self.IsAuthenticated - - @property - def pk(self): - return self.PK - - @property - def id(self): - return self.ID - - def set_password(self, raw_password): - self.SetPassword(raw_password) - - def check_password(self, raw_password): - return self.CheckPassword(raw_password) - - -# === 用户头像表 === - -class UserAvatar(QModel): - """用户头像表(映射 SAAS 主库 UserAvatar 表)""" - - UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') - UserAvatar = models.BinaryField(editable=True, db_column='UserAvatar', verbose_name='头像数据') - AvatarUploadTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='AvatarUploadTime', verbose_name='上传时间') - AvatarFileType = models.CharField(max_length=16, default='png', db_column='AvatarFileType', verbose_name='文件类型') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserAvatar' - verbose_name = '用户头像' - verbose_name_plural = '用户头像' - - -# === 用户双因素认证表 === - -class User2FA(QModel): - """用户双因素认证表(映射 SAAS 主库 User2FA 表)""" - - User2FAUUID = UUIDField(primary_key=True, db_column='User2FAUUID', verbose_name='2FA UUID') - UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') - SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') - IsEnabled = models.SmallIntegerField(default=0, db_column='IsEnabled', verbose_name='是否启用') - Verified = models.SmallIntegerField(default=0, db_column='Verified', verbose_name='已验证') - LastUsedAt = models.DateTimeField(null=True, blank=True, db_column='LastUsedAt', verbose_name='最后使用') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'User2FA' - verbose_name = '双因素认证' - verbose_name_plural = '双因素认证' - - -# === 双因素认证待验证表 === - -class UserPending2FA(QModel): - """双因素认证待验证表(映射 SAAS 主库 UserPending2FA 表)""" - - PendingUUID = UUIDField(primary_key=True, db_column='PendingUUID', verbose_name='待确认UUID') - UserUUID = UUIDField(unique=True, db_column='UserUUID', verbose_name='用户UUID') - SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') - ExpireAt = models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserPending2FA' - verbose_name = '2FA待确认' - verbose_name_plural = '2FA待确认' - - -# === 临时令牌表 === - -def _EltEndTimeDefault(): - from django.conf import settings - days = getattr(settings, 'ELT_TOKEN_LIFETIME_DAYS', 30) - return _datetime.datetime.utcnow() + _datetime.timedelta(days=days) - - -class EphemeralToken(QModel): - """临时令牌表——用于 API 认证与会话管理(映射 SAAS 主库 EphemeralToken 表)""" - - ELTUUID = UUIDField(primary_key=True, db_column='ELTUUID', verbose_name='令牌UUID') - ELTUserUUID = UUIDField(db_index=True, db_column='ELTUserUUID', verbose_name='用户UUID') - ELTCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='ELTCreateTime', verbose_name='创建时间') - ELTEndTime = models.DateTimeField(default=_EltEndTimeDefault, db_column='ELTEndTime', verbose_name='过期时间') - ELTType = models.SmallIntegerField(default=0, db_column='ELTType', verbose_name='令牌类型') - ELTAllowIP = models.JSONField(default=list, db_column='ELTAllowIP', verbose_name='允许IP') - ELTStatus = models.SmallIntegerField(default=1, db_index=True, db_column='ELTStatus', verbose_name='状态') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'EphemeralToken' - verbose_name = '临时令牌' - verbose_name_plural = '临时令牌' - - def IsExpired(self): - """检查令牌是否过期""" - return _datetime.datetime.utcnow() > self.ELTEndTime.replace(tzinfo=None) - - def IsValid(self): - """检查令牌是否有效""" - return self.ELTStatus == 1 and not self.IsExpired() - - -# === 用户登录日志表 === - -class UserLoginLog(QModel): - """用户登录日志表(映射 SAAS 主库 UserLoginLog 表)""" - - LogUUID = UUIDField(primary_key=True, db_column='LogUUID', verbose_name='日志UUID') - UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') - LoginIP = models.CharField(max_length=39, db_column='LoginIP', verbose_name='登录IP') - LoginPlatform = models.CharField(max_length=32, db_column='LoginPlatform', verbose_name='登录平台') - LoginStatus = models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态') - LoginTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='LoginTime', verbose_name='登录时间') - UserAgent = models.CharField(max_length=512, default='', db_column='UserAgent', verbose_name='用户代理') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserLoginLog' - verbose_name = '登录日志' - verbose_name_plural = '登录日志' - - -# === 账户激活码表 === - -class AccountActivation(QModel): - """账户激活码表(映射 SAAS 主库 AccountActivation 表)""" - - AccountActivationCode = FixedBinaryField(primary_key=True, binary_length=6, db_column='AccountActivationCode', verbose_name='激活码') - UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'AccountActivation' - verbose_name = '账户激活' - verbose_name_plural = '账户激活' - - -# === 角色表 === - -class Role(QModel): - """角色表——权限分配单元(映射 SAAS 主库 Role 表) - - 角色是真正承载操作权限的实体,在单个租户内部独立存在, - 不依附任何公司或部门。通过 AssignScope 控制角色的分配可见范围: - - 0:全租户管理员可分发 - - 1:仅指定公司管理员可分发(AssignScopeUUID = CompanyUUID) - - 2:仅指定部门管理员可分发(AssignScopeUUID = DeptUUID) - """ - - RoleUUID = UUIDField(primary_key=True, db_column='RoleUUID', verbose_name='角色UUID') - TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') - RoleName = models.CharField(max_length=128, db_column='RoleName', verbose_name='角色名称') - RoleType = models.SmallIntegerField(default=2, db_column='RoleType', verbose_name='角色类型') - RoleStatus = models.SmallIntegerField(default=1, db_column='RoleStatus', verbose_name='角色状态') - AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围') - AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID') - RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'Role' - verbose_name = '角色' - verbose_name_plural = '角色' - - def __str__(self): - return self.RoleName - - def GetPermissions(self): - """获取角色拥有的权限列表(通过 RolePermission 中间表)""" - perm_uuids = RolePermission.objects.filter( - RoleUUID=self.RoleUUID - ).values_list('PermUUID', flat=True) - return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - - def GetUsers(self): - """获取拥有该角色的用户列表""" - user_uuids = UserRole.objects.filter( - RoleUUID=self.RoleUUID - ).values_list('UserUUID', flat=True) - return User.objects.filter(UserUUID__in=user_uuids) - - -# === 权限表 === - -class Permission(QModel): - """权限表——操作权限定义(映射 SAAS 主库 Permission 表) - - 独立的权限定义表,租户内全局可用,不再挂在角色下。 - 通过 RolePermission 中间表与角色建立多对多关系, - 通过 PositionTemplatePermission 中间表与岗位模板建立多对多关系。 - """ - - PermUUID = UUIDField(primary_key=True, db_column='PermUUID', verbose_name='权限UUID') - TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') - PermName = models.CharField(max_length=128, db_column='PermName', verbose_name='权限名称') - PermCode = models.CharField(max_length=128, db_column='PermCode', verbose_name='权限键') - PermCategory = models.CharField(max_length=64, default='', db_column='PermCategory', verbose_name='权限分类') - PermStatus = models.SmallIntegerField(default=1, db_column='PermStatus', verbose_name='是否启用') - PermDesc = models.CharField(max_length=512, default='', db_column='PermDesc', verbose_name='描述') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'Permission' - verbose_name = '权限' - verbose_name_plural = '权限' - - def __str__(self): - return f"{self.PermName} ({self.PermCode})" - - def GetRoles(self): - """获取拥有该权限的角色列表""" - role_uuids = RolePermission.objects.filter( - PermUUID=self.PermUUID - ).values_list('RoleUUID', flat=True) - return Role.objects.filter(RoleUUID__in=role_uuids) - - -# === 用户角色关联表 === - -class UserRole(QModel): - """用户角色关联表——用户与角色的多对多映射(映射 SAAS 主库 UserRole 表) - - 通过 CompanyUUID 标识该用户-角色关联所属的公司, - 用于菜单按公司差异化显示、数据权限按公司过滤等场景。 - 同一用户在不同公司下可绑定不同角色。 - - 注意:UserUUID 和 RoleUUID 在 gvsds 中是 ForeignKey, - 但此处改为 UUIDField 以保持独立性。 - """ - - UserRoleUUID = UUIDField(primary_key=True, db_column='UserRoleUUID', verbose_name='关联UUID') - # 原 gvsds 中为 ForeignKey(User),此处改为 UUIDField - UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') - # 原 gvsds 中为 ForeignKey('Role'),此处改为 UUIDField - RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') - CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserRole' - verbose_name = '用户角色关联' - verbose_name_plural = '用户角色关联' - - -# === 角色权限关联表 === - -class RolePermission(QModel): - """角色权限关联表——角色与操作权限的多对多映射(映射 SAAS 主库 RolePermission 表) - - 一个角色可以拥有多个权限,一个权限也可以被多个角色引用。 - 角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。 - """ - - RolePermUUID = UUIDField(primary_key=True, db_column='RolePermUUID', verbose_name='关联UUID') - RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') - PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'RolePermission' - verbose_name = '角色权限关联' - verbose_name_plural = '角色权限关联' - - -# === 用户 ABAC 表 === - -class UserABAC(QModel): - """用户 ABAC 表——基于属性的访问控制(映射 SAAS 主库 UserABAC 表)""" - - UserABACUUID = UUIDField(primary_key=True, db_column='UserABACUUID', verbose_name='ABAC UUID') - UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') - HierarchicalPermissionIdentifiers = models.CharField(max_length=256, db_column='HierarchicalPermissionIdentifiers', verbose_name='层级权限标识') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserABAC' - verbose_name = '用户ABAC' - verbose_name_plural = '用户ABAC' - - -# === 密码表 === - -class Password(QModel): - """密码表——独立密码存储(映射 SAAS 主库 Password 表)""" - - UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') - Password = models.CharField(max_length=1024, db_column='Password', verbose_name='密码哈希') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'Password' - verbose_name = '密码' - verbose_name_plural = '密码' - - -# === 商铺表 === - -class Shop(QModel): - """商铺表——SAAS 多商铺体系中的商铺实体 - - 每个商铺属于一个公司(CompanyUUID),可关联多个角色(通过 RoleShop)。 - """ - - ShopUUID = UUIDField(primary_key=True, db_column='ShopUUID', verbose_name='商铺UUID') - CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') - ShopName = models.CharField(max_length=128, db_column='ShopName', verbose_name='商铺名称') - ShopCode = models.CharField(max_length=64, default='', db_column='ShopCode', verbose_name='商铺编码') - ShopType = models.SmallIntegerField(default=0, db_column='ShopType', verbose_name='商铺类型') - ShopStatus = models.SmallIntegerField(default=1, db_column='ShopStatus', verbose_name='商铺状态') - ShopAddress = models.CharField(max_length=512, default='', db_column='ShopAddress', verbose_name='商铺地址') - ShopContact = models.CharField(max_length=128, default='', db_column='ShopContact', verbose_name='联系人') - ShopPhone = models.CharField(max_length=32, default='', db_column='ShopPhone', verbose_name='联系电话') - ShopDesc = models.CharField(max_length=512, default='', db_column='ShopDesc', verbose_name='描述') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'Shop' - verbose_name = '商铺' - verbose_name_plural = '商铺' - - def __str__(self): - return self.ShopName - - def GetRoles(self): - """获取商铺关联的所有角色""" - role_uuids = RoleShop.objects.filter( - ShopUUID=self.ShopUUID - ).values_list('RoleUUID', flat=True) - return Role.objects.filter(RoleUUID__in=role_uuids) - - -# === 角色商铺关联表 === - -class RoleShop(QModel): - """角色商铺关联表——角色与商铺的多对多映射 - - 一个角色可以管理多个商铺,一个商铺也可以被多个角色管理。 - """ - - RoleShopUUID = UUIDField(primary_key=True, db_column='RoleShopUUID', verbose_name='关联UUID') - RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') - ShopUUID = UUIDField(db_column='ShopUUID', verbose_name='商铺UUID') - IsMainShop = models.SmallIntegerField(default=0, db_column='IsMainShop', verbose_name='主商铺') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'RoleShop' - verbose_name = '角色商铺关联' - verbose_name_plural = '角色商铺关联' - - -# === 岗位模板表 === - -class PositionTemplate(QModel): - """岗位模板表——权限初始化快照模板(映射 SAAS 主库 PositionTemplate 表) - - 租户内全局可重复使用,定位是权限初始化快照模板, - 不直接分配给员工、不参与运行时鉴权。 - 新建角色时可一键复制模板内的全部权限内容生成新角色。 - 模板后续调整只会作用于之后新创建的角色, - 已有角色不会同步变更,实现模板复用和存量权限独立管控互不干扰。 - """ - - PositionTemplateUUID = UUIDField(primary_key=True, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') - TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') - TemplateName = models.CharField(max_length=128, db_column='TemplateName', verbose_name='模板名称') - TemplateCode = models.CharField(max_length=64, default='', db_column='TemplateCode', verbose_name='模板编码') - TemplateStatus = models.SmallIntegerField(default=1, db_column='TemplateStatus', verbose_name='模板状态') - TemplateDesc = models.CharField(max_length=512, default='', db_column='TemplateDesc', verbose_name='模板描述') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'PositionTemplate' - verbose_name = '岗位模板' - verbose_name_plural = '岗位模板' - - def __str__(self): - return self.TemplateName - - def GetPermissions(self): - """获取模板关联的权限列表""" - perm_uuids = PositionTemplatePermission.objects.filter( - PositionTemplateUUID=self.PositionTemplateUUID - ).values_list('PermUUID', flat=True) - return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) - - def CopyToRole(self, RoleName, TenantUUID): - """从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色)""" - role = Role.objects.create( - RoleUUID=_uuid.uuid4().bytes, - TenantUUID=TenantUUID, - RoleName=RoleName, - ) - template_perms = PositionTemplatePermission.objects.filter( - PositionTemplateUUID=self.PositionTemplateUUID - ) - for tp in template_perms: - RolePermission.objects.create( - RolePermUUID=_uuid.uuid4().bytes, - RoleUUID=role.RoleUUID, - PermUUID=tp.PermUUID, - ) - return role - - -# === 岗位模板权限关联表 === - -class PositionTemplatePermission(QModel): - """岗位模板权限关联表——岗位模板与操作权限的多对多映射 - - 仅作为复制源头,不参与运行时鉴权。 - 新建角色时一键复制模板权限到角色,复制后模板与角色无运行时绑定。 - """ - - PTPermUUID = UUIDField(primary_key=True, db_column='PTPermUUID', verbose_name='关联UUID') - PositionTemplateUUID = UUIDField(db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') - PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'PositionTemplatePermission' - verbose_name = '岗位模板权限关联' - verbose_name_plural = '岗位模板权限关联' - - -# === 人事职务标签表 === - -class Position(QModel): - """人事职务标签表——仅用于人事管理标注(映射 SAAS 主库 Position 表) - - 员工会标注对应的职务标签,该标签仅为人事标注信息, - 不直接绑定权限,和角色、权限体系完全解耦。 - 人员调岗、升职只修改组织归属和人事职务标签,权限只需增删绑定角色即可,互不干扰。 - """ - - PositionUUID = UUIDField(primary_key=True, db_column='PositionUUID', verbose_name='职务UUID') - TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') - PositionName = models.CharField(max_length=128, db_column='PositionName', verbose_name='职务名称') - PositionCode = models.CharField(max_length=64, default='', db_column='PositionCode', verbose_name='职务编码') - PositionLevel = models.SmallIntegerField(default=0, db_column='PositionLevel', verbose_name='职务级别') - PositionStatus = models.SmallIntegerField(default=1, db_column='PositionStatus', verbose_name='状态') - PositionDesc = models.CharField(max_length=512, default='', db_column='PositionDesc', verbose_name='描述') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'Position' - verbose_name = '人事职务标签' - verbose_name_plural = '人事职务标签' - - def __str__(self): - return self.PositionName - - -# === 员工部门关联表 === - -class UserDept(QModel): - """员工部门关联表——员工与部门的多对多映射(映射 SAAS 主库 UserDept 表) - - 支持一人多部门兼职,同时标注该员工在对应部门的职务标签。 - 该层级仅用于组织架构展示、审批流转、人员统计, - 同时为数据权限判断提供人员所属组织属性依据,不参与操作权限校验。 - """ - - UserDeptUUID = UUIDField(primary_key=True, db_column='UserDeptUUID', verbose_name='关联UUID') - UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') - DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID') - PositionUUID = UUIDField(null=True, blank=True, db_column='PositionUUID', verbose_name='职务标签UUID') - IsMainDept = models.SmallIntegerField(default=1, db_column='IsMainDept', verbose_name='是否主部门') - CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') - - class Meta: - managed = False - app_label = 'gvsdsdk' - db_table = 'UserDept' - verbose_name = '员工部门关联' - verbose_name_plural = '员工部门关联' +"""GVS-DSDK 数据模型模块 + +将 SAAS 主服务器的用户表(User)、权限表(Permission)、角色表(Role)、 +用户角色关联表(UserRole)、角色权限关联表(RolePermission)、 +岗位模板表(PositionTemplate)、岗位模板权限关联表(PositionTemplatePermission)、 +人事职务标签表(Position)、员工部门关联表(UserDept)封装为独立的 Django ORM 模型, +供子服务器直接进行跨服务器 ORM 查询。 + +权限体系四大分层: + 1. 组织人事层:Tenant → Company → Department,员工通过 UserDept 归属部门 + 2. 岗位模板层:PositionTemplate + PositionTemplatePermission,权限初始化快照 + 3. 角色权限载体层:Role + RolePermission,运行时鉴权实体 + 4. 鉴权规则层:操作权限校验(角色汇总)+ 数据权限校验(组织属性) + +使用方式(子服务器): + from gvsdsdk.models import Permission, Role, UserRole, RolePermission + + # 查询所有启用的用户(需在业务子类中定义具体 User 模型) + users = User.objects.filter(UserAccountLicense=1) + + # 或配置 DatabaseRouter 后直接使用 + users = User.objects.filter(UserAccountLicense=1) + +设计原则: + 1. 独立:不依赖 gvsds 内部模块,可在任意 Django 项目中独立使用 + 2. 非托管:managed=False,不创建/迁移表,仅映射已有表结构 + 3. PascalCase:字段名、方法名遵循帕斯卡命名法 + 4. 兼容:db_table、db_column 与 SAAS 主库完全一致 + 5. 租户隔离:所有权限相关数据依附于独立租户空间,不同租户互相不可见 + +注意:User 为抽象基类,仅包含 SAAS 主库标准字段。 + 业务子类应继承 User 并在单独文件中添加业务字段和方法。 + 参见 gvsdsdk/business_models.py。 +""" + +import uuid as _uuid +import datetime as _datetime +from django.db import models +from django.contrib.auth.models import BaseUserManager +from gvsdsdk.model_base import QModel +from gvsdsdk.model_utils import UUIDObj + + +# === UUID 字段 === + +class FixedBinaryField(models.BinaryField): + """定长二进制字段:映射 MySQL BINARY(N),可用于主键""" + + description = "Fixed-length binary field" + + def __init__(self, *args, **kwargs): + self.binary_length = kwargs.pop('binary_length', 16) + kwargs.setdefault('max_length', self.binary_length) + kwargs.setdefault('editable', False) + super().__init__(*args, **kwargs) + + def db_type(self, connection): + if connection.vendor == 'mysql': + return f'BINARY({self.binary_length})' + elif connection.vendor == 'sqlite': + return 'BLOB' + elif connection.vendor == 'postgresql': + return 'BYTEA' + return f'BINARY({self.binary_length})' + + def rel_db_type(self, connection): + return self.db_type(connection) + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + kwargs['binary_length'] = self.binary_length + return name, path, args, kwargs + + +class UUIDField(FixedBinaryField): + """UUID 字段:数据库存储为 BINARY(16),Python 侧自动转换 str/bytes/uuid.UUID""" + + description = "UUID stored as BINARY(16)" + + def __init__(self, *args, **kwargs): + kwargs.setdefault('max_length', 16) + kwargs.setdefault('editable', False) + super().__init__(*args, **kwargs) + + def db_type(self, connection): + vendor = connection.vendor + if vendor == 'mysql': + return 'BINARY(16)' + elif vendor == 'sqlite': + return 'BLOB' + elif vendor == 'postgresql': + return 'BYTEA' + return 'BINARY(16)' + + def from_db_value(self, value, expression, connection): + if value is None: + return None + if isinstance(value, UUIDObj): + return value + return UUIDObj(value) + + def get_prep_value(self, value): + if value is None: + return None + if isinstance(value, UUIDObj): + return value.bytes + if isinstance(value, str): + try: + return _uuid.UUID(value).bytes + except ValueError: + return None + if isinstance(value, _uuid.UUID): + return value.bytes + if isinstance(value, bytes): + return value + return None + + def get_prep_lookup(self, lookup_type, value): + if lookup_type in ('exact', 'iexact', 'in'): + if value is None: + return None + if lookup_type == 'in' and isinstance(value, (list, tuple)): + return [self.get_prep_value(v) for v in value] + return self.get_prep_value(value) + return super().get_prep_lookup(lookup_type, value) + + def value_to_string(self, obj): + value = self.value_from_object(obj) + if isinstance(value, UUIDObj): + return value.str or '' + if isinstance(value, str): + return value + if isinstance(value, bytes) and len(value) == 16: + return str(_uuid.UUID(bytes=value)) + if isinstance(value, _uuid.UUID): + return str(value) + return '' + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + del kwargs['max_length'] + return name, path, args, kwargs + + +# === QModel 基础模型 === + +# === 用户管理器 === + +class UserManager(BaseUserManager): + """用户管理器——提供用户创建与超级用户创建。 + 方法名保留蛇形命名以兼容 Django 内部约定。""" + + def create_user(self, username, password=None, email='', **extra_fields): + """创建普通用户""" + if not username: + raise ValueError('The Username field must be set') + user = self.model( + UserUUID=_uuid.uuid4().bytes, + UserName=username, + UserEmail=email or '', + **extra_fields + ) + if password: + user.SetPassword(password) + user.save(using=self._db) + return user + + def create_superuser(self, username, password=None, **extra_fields): + """创建超级用户""" + extra_fields.setdefault('UserAccountLicense', 1) + extra_fields.setdefault('UserPositionStatus', 0) + extra_fields.setdefault('IsStaff', True) + extra_fields.setdefault('IsSuperuser', True) + return self.create_user(username, password, **extra_fields) + + +# === 用户表 === +class User(QModel): + """用户表——系统核心身份模型(抽象基类,映射 SAAS 主库 User 表标准字段) + + 本类为抽象基类,仅包含 SAAS 主库 User 表的标准字段和方法。 + 业务子类应继承本类,在单独文件中添加业务字段和方法。 + 参见 gvsdsdk/business_models.py。 + """ + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=_uuid.uuid4) + UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名') + UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码') + UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态') + UserPersonalSignature = models.CharField(max_length=1024, default='', db_column='UserPersonalSignature', verbose_name='个人签名') + UserEmail = models.CharField(max_length=256, default='', db_column='UserEmail', verbose_name='邮箱') + UserEmailVerify = models.SmallIntegerField(default=0, db_column='UserEmailVerify', verbose_name='邮箱验证') + UserAccountLicense = models.SmallIntegerField(default=1, db_column='UserAccountLicense', verbose_name='账户许可') + UserGender = models.SmallIntegerField(default=-1, db_column='UserGender', verbose_name='性别') + UserTele = models.CharField(max_length=16, null=True, blank=True, db_column='UserTele', verbose_name='电话') + UserCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserCreateTime', verbose_name='创建时间') + UserLastLoginDate = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UserLastLoginDate', verbose_name='最后登录') + + # Django auth 框架必需(由具体子类覆盖) + USERNAME_FIELD = 'UserName' + REQUIRED_FIELDS = [] + + class Meta: + abstract = True + + def __str__(self): + return self.UserName + + @property + def UUIDStr(self): + """返回 UUID 字符串形式""" + return UUIDObj(self.UserUUID).str + + @property + def IsActive(self): + """账户是否启用(UserAccountLicense == 1)""" + return self.UserAccountLicense == 1 + + @property + def IsOnJob(self): + """是否在职(UserPositionStatus == 0)""" + return self.UserPositionStatus == 0 + + @property + def IsAnonymous(self): + return False + + @property + def IsAuthenticated(self): + return True + + @property + def PK(self): + return UUIDObj(self.UserUUID) + + @property + def ID(self): + return UUIDObj(self.UserUUID) + + def SetPassword(self, RawPassword): + """设置密码——bcrypt 哈希""" + import bcrypt + salt = bcrypt.gensalt(rounds=12) + hashed = bcrypt.hashpw(RawPassword.encode('utf-8'), salt) + self.UserPassword = hashed.decode('utf-8') + + def CheckPassword(self, RawPassword): + """验证密码——兼容 bcrypt 哈希和明文存储""" + import bcrypt + stored = self.UserPassword or '' + # bcrypt 哈希以 $2b$ 或 $2a$ 开头,长度固定 60 + if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60: + try: + return bcrypt.checkpw( + RawPassword.encode('utf-8'), + stored.encode('utf-8') + ) + except Exception: + return False + else: + # 明文密码(旧数据兼容) + return stored == RawPassword + + def GetTele(self): + """获取电话,空则返回默认文本""" + return self.UserTele or '未填写' + + def GetCompanies(self): + """获取用户所属公司列表""" + # Company 模型不在 gvsdsdk 中,此处返回空 QuerySet + # 如需使用,请在子服务器中自行实现 + return [] + + def GetRoles(self): + """获取用户角色列表(需在同一 database 连接下使用)""" + user_role_uuids = UserRole.objects.filter( + UserUUID=self.UserUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=user_role_uuids) + + def GetDepartments(self): + """获取用户部门列表""" + # Department 模型不在 gvsdsdk 中,此处返回空列表 + # 如需使用,请在子服务器中自行实现 + return [] + + def HasPermission(self, PermCode): + """检查用户是否拥有指定权限(需在同一 database 连接下使用) + + 通过 UserRole → RolePermission → Permission 链路查询, + 汇总所有角色的操作权限后判断。 + """ + user_role_uuids = UserRole.objects.filter( + UserUUID=self.UserUUID + ).values_list('RoleUUID', flat=True) + perm_uuids = RolePermission.objects.filter( + RoleUUID__in=user_role_uuids + ).values_list('PermUUID', flat=True) + return Permission.objects.filter( + PermUUID__in=perm_uuids, + PermCode=PermCode, + PermStatus=1, + ).exists() + + # ── Django auth 框架必需属性 ── + @property + def is_active(self): + return self.IsActive + + @property + def is_anonymous(self): + return self.IsAnonymous + + @property + def is_authenticated(self): + return self.IsAuthenticated + + @property + def pk(self): + return self.PK + + @property + def id(self): + return self.ID + + def set_password(self, raw_password): + self.SetPassword(raw_password) + + def check_password(self, raw_password): + return self.CheckPassword(raw_password) + + +# === 用户头像表 === + +class UserAvatar(QModel): + """用户头像表(映射 SAAS 主库 UserAvatar 表)""" + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') + UserAvatar = models.BinaryField(editable=True, db_column='UserAvatar', verbose_name='头像数据') + AvatarUploadTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='AvatarUploadTime', verbose_name='上传时间') + AvatarFileType = models.CharField(max_length=16, default='png', db_column='AvatarFileType', verbose_name='文件类型') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserAvatar' + verbose_name = '用户头像' + verbose_name_plural = '用户头像' + + +# === 用户双因素认证表 === + +class User2FA(QModel): + """用户双因素认证表(映射 SAAS 主库 User2FA 表)""" + + User2FAUUID = UUIDField(primary_key=True, db_column='User2FAUUID', verbose_name='2FA UUID') + UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') + SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') + IsEnabled = models.SmallIntegerField(default=0, db_column='IsEnabled', verbose_name='是否启用') + Verified = models.SmallIntegerField(default=0, db_column='Verified', verbose_name='已验证') + LastUsedAt = models.DateTimeField(null=True, blank=True, db_column='LastUsedAt', verbose_name='最后使用') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'User2FA' + verbose_name = '双因素认证' + verbose_name_plural = '双因素认证' + + +# === 双因素认证待验证表 === + +class UserPending2FA(QModel): + """双因素认证待验证表(映射 SAAS 主库 UserPending2FA 表)""" + + PendingUUID = UUIDField(primary_key=True, db_column='PendingUUID', verbose_name='待确认UUID') + UserUUID = UUIDField(unique=True, db_column='UserUUID', verbose_name='用户UUID') + SecretKey = models.CharField(max_length=64, db_column='SecretKey', verbose_name='密钥') + ExpireAt = models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserPending2FA' + verbose_name = '2FA待确认' + verbose_name_plural = '2FA待确认' + + +# === 临时令牌表 === + +def _EltEndTimeDefault(): + from django.conf import settings + days = getattr(settings, 'ELT_TOKEN_LIFETIME_DAYS', 30) + return _datetime.datetime.utcnow() + _datetime.timedelta(days=days) + + +class EphemeralToken(QModel): + """临时令牌表——用于 API 认证与会话管理(映射 SAAS 主库 EphemeralToken 表)""" + + ELTUUID = UUIDField(primary_key=True, db_column='ELTUUID', verbose_name='令牌UUID') + ELTUserUUID = UUIDField(db_index=True, db_column='ELTUserUUID', verbose_name='用户UUID') + ELTCreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='ELTCreateTime', verbose_name='创建时间') + ELTEndTime = models.DateTimeField(default=_EltEndTimeDefault, db_column='ELTEndTime', verbose_name='过期时间') + ELTType = models.SmallIntegerField(default=0, db_column='ELTType', verbose_name='令牌类型') + ELTAllowIP = models.JSONField(default=list, db_column='ELTAllowIP', verbose_name='允许IP') + ELTStatus = models.SmallIntegerField(default=1, db_index=True, db_column='ELTStatus', verbose_name='状态') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'EphemeralToken' + verbose_name = '临时令牌' + verbose_name_plural = '临时令牌' + + def IsExpired(self): + """检查令牌是否过期""" + return _datetime.datetime.utcnow() > self.ELTEndTime.replace(tzinfo=None) + + def IsValid(self): + """检查令牌是否有效""" + return self.ELTStatus == 1 and not self.IsExpired() + + +# === 用户登录日志表 === + +class UserLoginLog(QModel): + """用户登录日志表(映射 SAAS 主库 UserLoginLog 表)""" + + LogUUID = UUIDField(primary_key=True, db_column='LogUUID', verbose_name='日志UUID') + UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID') + LoginIP = models.CharField(max_length=39, db_column='LoginIP', verbose_name='登录IP') + LoginPlatform = models.CharField(max_length=32, db_column='LoginPlatform', verbose_name='登录平台') + LoginStatus = models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态') + LoginTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='LoginTime', verbose_name='登录时间') + UserAgent = models.CharField(max_length=512, default='', db_column='UserAgent', verbose_name='用户代理') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserLoginLog' + verbose_name = '登录日志' + verbose_name_plural = '登录日志' + + +# === 账户激活码表 === + +class AccountActivation(QModel): + """账户激活码表(映射 SAAS 主库 AccountActivation 表)""" + + AccountActivationCode = FixedBinaryField(primary_key=True, binary_length=6, db_column='AccountActivationCode', verbose_name='激活码') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'AccountActivation' + verbose_name = '账户激活' + verbose_name_plural = '账户激活' + + +# === 角色表 === + +class Role(QModel): + """角色表——权限分配单元(映射 SAAS 主库 Role 表) + + 角色是真正承载操作权限的实体,在单个租户内部独立存在, + 不依附任何公司或部门。通过 AssignScope 控制角色的分配可见范围: + - 0:全租户管理员可分发 + - 1:仅指定公司管理员可分发(AssignScopeUUID = CompanyUUID) + - 2:仅指定部门管理员可分发(AssignScopeUUID = DeptUUID) + """ + + RoleUUID = UUIDField(primary_key=True, db_column='RoleUUID', verbose_name='角色UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + RoleName = models.CharField(max_length=128, db_column='RoleName', verbose_name='角色名称') + RoleType = models.SmallIntegerField(default=2, db_column='RoleType', verbose_name='角色类型') + RoleStatus = models.SmallIntegerField(default=1, db_column='RoleStatus', verbose_name='角色状态') + AssignScope = models.SmallIntegerField(default=0, db_column='AssignScope', verbose_name='分配可见范围') + AssignScopeUUID = UUIDField(null=True, blank=True, db_column='AssignScopeUUID', verbose_name='分配范围UUID') + RoleDesc = models.CharField(max_length=512, default='', db_column='RoleDesc', verbose_name='角色描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'Role' + verbose_name = '角色' + verbose_name_plural = '角色' + + def __str__(self): + return self.RoleName + + def GetPermissions(self): + """获取角色拥有的权限列表(通过 RolePermission 中间表)""" + perm_uuids = RolePermission.objects.filter( + RoleUUID=self.RoleUUID + ).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + def GetUsers(self): + """获取拥有该角色的用户列表""" + from django.apps import apps + UserModel = apps.get_model('gvsdsdk', 'User') + user_uuids = UserRole.objects.filter( + RoleUUID=self.RoleUUID + ).values_list('UserUUID', flat=True) + return UserModel.objects.filter(UserUUID__in=user_uuids) + + +# === 权限表 === + +class Permission(QModel): + """权限表——操作权限定义(映射 SAAS 主库 Permission 表) + + 独立的权限定义表,租户内全局可用,不再挂在角色下。 + 通过 RolePermission 中间表与角色建立多对多关系, + 通过 PositionTemplatePermission 中间表与岗位模板建立多对多关系。 + """ + + PermUUID = UUIDField(primary_key=True, db_column='PermUUID', verbose_name='权限UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + PermName = models.CharField(max_length=128, db_column='PermName', verbose_name='权限名称') + PermCode = models.CharField(max_length=128, db_column='PermCode', verbose_name='权限键') + PermCategory = models.CharField(max_length=64, default='', db_column='PermCategory', verbose_name='权限分类') + PermStatus = models.SmallIntegerField(default=1, db_column='PermStatus', verbose_name='是否启用') + PermDesc = models.CharField(max_length=512, default='', db_column='PermDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'Permission' + verbose_name = '权限' + verbose_name_plural = '权限' + + def __str__(self): + return f"{self.PermName} ({self.PermCode})" + + def GetRoles(self): + """获取拥有该权限的角色列表""" + role_uuids = RolePermission.objects.filter( + PermUUID=self.PermUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + +# === 用户角色关联表 === + +class UserRole(QModel): + """用户角色关联表——用户与角色的多对多映射(映射 SAAS 主库 UserRole 表) + + 通过 CompanyUUID 标识该用户-角色关联所属的公司, + 用于菜单按公司差异化显示、数据权限按公司过滤等场景。 + 同一用户在不同公司下可绑定不同角色。 + + 注意:UserUUID 和 RoleUUID 在 gvsds 中是 ForeignKey, + 但此处改为 UUIDField 以保持独立性。 + """ + + UserRoleUUID = UUIDField(primary_key=True, db_column='UserRoleUUID', verbose_name='关联UUID') + # 原 gvsds 中为 ForeignKey(User),此处改为 UUIDField + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + # 原 gvsds 中为 ForeignKey('Role'),此处改为 UUIDField + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserRole' + verbose_name = '用户角色关联' + verbose_name_plural = '用户角色关联' + + +# === 角色权限关联表 === + +class RolePermission(QModel): + """角色权限关联表——角色与操作权限的多对多映射(映射 SAAS 主库 RolePermission 表) + + 一个角色可以拥有多个权限,一个权限也可以被多个角色引用。 + 角色和具体操作权限为多对多绑定关系,一套权限集合可以打包在一个角色内统一维护。 + """ + + RolePermUUID = UUIDField(primary_key=True, db_column='RolePermUUID', verbose_name='关联UUID') + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'RolePermission' + verbose_name = '角色权限关联' + verbose_name_plural = '角色权限关联' + + +# === 用户 ABAC 表 === + +class UserABAC(QModel): + """用户 ABAC 表——基于属性的访问控制(映射 SAAS 主库 UserABAC 表)""" + + UserABACUUID = UUIDField(primary_key=True, db_column='UserABACUUID', verbose_name='ABAC UUID') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + HierarchicalPermissionIdentifiers = models.CharField(max_length=256, db_column='HierarchicalPermissionIdentifiers', verbose_name='层级权限标识') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserABAC' + verbose_name = '用户ABAC' + verbose_name_plural = '用户ABAC' + + +# === 密码表 === + +class Password(QModel): + """密码表——独立密码存储(映射 SAAS 主库 Password 表)""" + + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID') + Password = models.CharField(max_length=1024, db_column='Password', verbose_name='密码哈希') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'Password' + verbose_name = '密码' + verbose_name_plural = '密码' + + +# === 商铺表 === + +class Shop(QModel): + """商铺表——SAAS 多商铺体系中的商铺实体 + + 每个商铺属于一个公司(CompanyUUID),可关联多个角色(通过 RoleShop)。 + """ + + ShopUUID = UUIDField(primary_key=True, db_column='ShopUUID', verbose_name='商铺UUID') + CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') + ShopName = models.CharField(max_length=128, db_column='ShopName', verbose_name='商铺名称') + ShopCode = models.CharField(max_length=64, default='', db_column='ShopCode', verbose_name='商铺编码') + ShopType = models.SmallIntegerField(default=0, db_column='ShopType', verbose_name='商铺类型') + ShopStatus = models.SmallIntegerField(default=1, db_column='ShopStatus', verbose_name='商铺状态') + ShopAddress = models.CharField(max_length=512, default='', db_column='ShopAddress', verbose_name='商铺地址') + ShopContact = models.CharField(max_length=128, default='', db_column='ShopContact', verbose_name='联系人') + ShopPhone = models.CharField(max_length=32, default='', db_column='ShopPhone', verbose_name='联系电话') + ShopDesc = models.CharField(max_length=512, default='', db_column='ShopDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'Shop' + verbose_name = '商铺' + verbose_name_plural = '商铺' + + def __str__(self): + return self.ShopName + + def GetRoles(self): + """获取商铺关联的所有角色""" + role_uuids = RoleShop.objects.filter( + ShopUUID=self.ShopUUID + ).values_list('RoleUUID', flat=True) + return Role.objects.filter(RoleUUID__in=role_uuids) + + +# === 角色商铺关联表 === + +class RoleShop(QModel): + """角色商铺关联表——角色与商铺的多对多映射 + + 一个角色可以管理多个商铺,一个商铺也可以被多个角色管理。 + """ + + RoleShopUUID = UUIDField(primary_key=True, db_column='RoleShopUUID', verbose_name='关联UUID') + RoleUUID = UUIDField(db_column='RoleUUID', verbose_name='角色UUID') + ShopUUID = UUIDField(db_column='ShopUUID', verbose_name='商铺UUID') + IsMainShop = models.SmallIntegerField(default=0, db_column='IsMainShop', verbose_name='主商铺') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'RoleShop' + verbose_name = '角色商铺关联' + verbose_name_plural = '角色商铺关联' + + +# === 岗位模板表 === + +class PositionTemplate(QModel): + """岗位模板表——权限初始化快照模板(映射 SAAS 主库 PositionTemplate 表) + + 租户内全局可重复使用,定位是权限初始化快照模板, + 不直接分配给员工、不参与运行时鉴权。 + 新建角色时可一键复制模板内的全部权限内容生成新角色。 + 模板后续调整只会作用于之后新创建的角色, + 已有角色不会同步变更,实现模板复用和存量权限独立管控互不干扰。 + """ + + PositionTemplateUUID = UUIDField(primary_key=True, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + TemplateName = models.CharField(max_length=128, db_column='TemplateName', verbose_name='模板名称') + TemplateCode = models.CharField(max_length=64, default='', db_column='TemplateCode', verbose_name='模板编码') + TemplateStatus = models.SmallIntegerField(default=1, db_column='TemplateStatus', verbose_name='模板状态') + TemplateDesc = models.CharField(max_length=512, default='', db_column='TemplateDesc', verbose_name='模板描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'PositionTemplate' + verbose_name = '岗位模板' + verbose_name_plural = '岗位模板' + + def __str__(self): + return self.TemplateName + + def GetPermissions(self): + """获取模板关联的权限列表""" + perm_uuids = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=self.PositionTemplateUUID + ).values_list('PermUUID', flat=True) + return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1) + + def CopyToRole(self, RoleName, TenantUUID): + """从模板复制权限到新角色(一次性复制,后续模板变更不影响已有角色)""" + role = Role.objects.create( + RoleUUID=_uuid.uuid4().bytes, + TenantUUID=TenantUUID, + RoleName=RoleName, + ) + template_perms = PositionTemplatePermission.objects.filter( + PositionTemplateUUID=self.PositionTemplateUUID + ) + for tp in template_perms: + RolePermission.objects.create( + RolePermUUID=_uuid.uuid4().bytes, + RoleUUID=role.RoleUUID, + PermUUID=tp.PermUUID, + ) + return role + + +# === 岗位模板权限关联表 === + +class PositionTemplatePermission(QModel): + """岗位模板权限关联表——岗位模板与操作权限的多对多映射 + + 仅作为复制源头,不参与运行时鉴权。 + 新建角色时一键复制模板权限到角色,复制后模板与角色无运行时绑定。 + """ + + PTPermUUID = UUIDField(primary_key=True, db_column='PTPermUUID', verbose_name='关联UUID') + PositionTemplateUUID = UUIDField(db_column='PositionTemplateUUID', verbose_name='岗位模板UUID') + PermUUID = UUIDField(db_column='PermUUID', verbose_name='权限UUID') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'PositionTemplatePermission' + verbose_name = '岗位模板权限关联' + verbose_name_plural = '岗位模板权限关联' + + +# === 人事职务标签表 === + +class Position(QModel): + """人事职务标签表——仅用于人事管理标注(映射 SAAS 主库 Position 表) + + 员工会标注对应的职务标签,该标签仅为人事标注信息, + 不直接绑定权限,和角色、权限体系完全解耦。 + 人员调岗、升职只修改组织归属和人事职务标签,权限只需增删绑定角色即可,互不干扰。 + """ + + PositionUUID = UUIDField(primary_key=True, db_column='PositionUUID', verbose_name='职务UUID') + TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID') + PositionName = models.CharField(max_length=128, db_column='PositionName', verbose_name='职务名称') + PositionCode = models.CharField(max_length=64, default='', db_column='PositionCode', verbose_name='职务编码') + PositionLevel = models.SmallIntegerField(default=0, db_column='PositionLevel', verbose_name='职务级别') + PositionStatus = models.SmallIntegerField(default=1, db_column='PositionStatus', verbose_name='状态') + PositionDesc = models.CharField(max_length=512, default='', db_column='PositionDesc', verbose_name='描述') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + UpdateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='UpdateTime', verbose_name='更新时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'Position' + verbose_name = '人事职务标签' + verbose_name_plural = '人事职务标签' + + def __str__(self): + return self.PositionName + + +# === 员工部门关联表 === + +class UserDept(QModel): + """员工部门关联表——员工与部门的多对多映射(映射 SAAS 主库 UserDept 表) + + 支持一人多部门兼职,同时标注该员工在对应部门的职务标签。 + 该层级仅用于组织架构展示、审批流转、人员统计, + 同时为数据权限判断提供人员所属组织属性依据,不参与操作权限校验。 + """ + + UserDeptUUID = UUIDField(primary_key=True, db_column='UserDeptUUID', verbose_name='关联UUID') + UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID') + DeptUUID = UUIDField(db_column='DeptUUID', verbose_name='部门UUID') + PositionUUID = UUIDField(null=True, blank=True, db_column='PositionUUID', verbose_name='职务标签UUID') + IsMainDept = models.SmallIntegerField(default=1, db_column='IsMainDept', verbose_name='是否主部门') + CreateTime = models.DateTimeField(default=_datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') + + class Meta: + managed = True + app_label = 'gvsdsdk' + db_table = 'UserDept' + verbose_name = '员工部门关联' + verbose_name_plural = '员工部门关联' diff --git a/gvsdsdk/msync/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/msync/__pycache__/__init__.cpython-312.pyc index 6f7f75707d2b3a399b986172697157a25b843ae3..a81552724e8f0d4ffae053174c830b118355d5ba 100644 GIT binary patch delta 15 WcmdnXw}o$m9V?Tx-eynMFN^>vR|LWU delta 24 fcmdnOx0i2&9V@p=id9UoucM!%yX$5L)-Q|zUy28l diff --git a/gvsdsdk/msync/__pycache__/auth.cpython-312.pyc b/gvsdsdk/msync/__pycache__/auth.cpython-312.pyc index a6027afa7a7647ccbb64f47154d203701df4c07f..5a3ee337395649ed2fc372652a2a155688918752 100644 GIT binary patch delta 28 kcmexu_{ngC6f@(m$-0H5{>TmS$7 delta 37 tcmexl_}g%U6f@(k$D0ZkA?_)nvT2xl-p57XU284rKrU diff --git a/gvsdsdk/msync/__pycache__/local_tx.cpython-312.pyc b/gvsdsdk/msync/__pycache__/local_tx.cpython-312.pyc index 10731ca38d62b6f4ffbebccbc05629f562695c9e..7af0209106e3122cea8913404398e44c4b5c34ac 100644 GIT binary patch delta 41 ucmaE-^;C=dG%qg~0}zDI)8ELQ$IK+HH@Ss*E#t4vMl9<%fz(6ZUUmQje+^Io delta 50 zcmaE=^-hcXG%qg~0}xz0Zm^L%kC|H~#VRJ)*U`_>-F0#`^IFDRn>AV1aRR9uyuIuI Dk1P;) diff --git a/gvsdsdk/msync/__pycache__/ogm.cpython-312.pyc b/gvsdsdk/msync/__pycache__/ogm.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9624dd360e40d89953226fa7aa9cebec1a209839 GIT binary patch literal 366 zcmX@j%ge<81le9XS=WH{V-N=hn4pZ$3P8qmh7^V!fVZftlqW5Lr6E${bqK3lQy z>D=jDTrZ}zJl)XvVsY#HJ+q$A-tw$(>5IKv^(c?bH3u&sHpaI(Ir3*NbT_Pd7BaSls%4 z&#dROw>;}x`eN^zr~MP3c5F~cFDp(d)=MlXDAvm@uFOl;D@skzEG{XkRCwAq|M`^p zPd7F_ZfN3q338Ao(=Gns)S|M?#X!1fl={ delta 23 ecmcb}c8hJpUnXu97ps_HUq?Sjch|}PnSualGYC=u diff --git a/gvsdsdk/order/__pycache__/models.cpython-312.pyc b/gvsdsdk/order/__pycache__/models.cpython-312.pyc index 2d0ca1e3522189b48fa9f8f3c2a5906ea2dea0cf..2f51f5aa56eb867301d9d8c3fa5cc203ecc69de8 100644 GIT binary patch delta 17 YcmbQao^ir@#tr&hOp_*eF1dk2oL}O diff --git a/gvsdsdk/payment/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/payment/__pycache__/__init__.cpython-312.pyc index 6f302a8f90112a36a72f4f956132d236184a8e55..89b25145bfd0a1dad28fc65c3c7ef931ee3ffb3c 100644 GIT binary patch delta 15 XcmX@jcZhGp6jml#z0LDk*DwMAE)fMp delta 24 gcmX@acbad*6jpAv6swqEUq?Sjch}9+Sl2KD0BKkV6#xJL diff --git a/gvsdsdk/payment/__pycache__/manager.cpython-312.pyc b/gvsdsdk/payment/__pycache__/manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..119b276a3e633b8aefbbf26699bdb98ce09f4672 GIT binary patch literal 17858 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 literal 0 HcmV?d00001 diff --git a/gvsdsdk/payment/__pycache__/models.cpython-312.pyc b/gvsdsdk/payment/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..623da4aa7287b6b9923735cf8beb77432cd44ba2 GIT binary patch literal 11047 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 literal 0 HcmV?d00001 diff --git a/gvsdsdk/payment/gateways/__pycache__/__init__.cpython-312.pyc b/gvsdsdk/payment/gateways/__pycache__/__init__.cpython-312.pyc index 1759c2a81f767284e747a0f911552e2df72ae6e4..449da236f5295ac2e3f0307c5408218599d575e7 100644 GIT binary patch delta 14 VcmX@Www-N*GZT}V-efRxIb`%3kQ>u-e!M}N(BHgD+L<> delta 24 fcmdlVcp`9v3kSD$id9UoucM!%yX$5*j!FdpZE6SX diff --git a/gvsdsdk/payment/gateways/__pycache__/registry.cpython-312.pyc b/gvsdsdk/payment/gateways/__pycache__/registry.cpython-312.pyc index 2bf2fd2e4978b2136ce281509b7bee4a509ecab1..fbb8cda0956ccb35f29c9304bc8ffe35b484c419 100644 GIT binary patch delta 15 Wcmdlfzd?ROFEf*x-sTz1iaY=;jRd&> delta 24 fcmdlWzf*ogFEh7(id9UoucM!%yX)qDWXm0CNrq_y7O^