diff --git a/SAAS_ARCHITECTURE.md b/SAAS_ARCHITECTURE.md new file mode 100644 index 00000000..b2a83ea3 --- /dev/null +++ b/SAAS_ARCHITECTURE.md @@ -0,0 +1,269 @@ +# SAAS 架构概览 + +> 面向技术决策会议的架构说明文档。本文档聚焦 SAAS 多租户-公司-部门-用户-角色架构设计、当前实现状态与未来发展方向。 + +--- + +## 1. 项目定位 + +**GVS-DSDK** 是一套面向中小企业(SMB)的 SAAS 化企业管理平台,包含: + +- **多租户 SAAS 框架**:支持租户入驻、审核、隔离 +- **企业资源管理(GERP)**:组织、角色、权限、菜单、工单、资产、文档 +- **三大业务引擎**:分佣引擎、支付网关、统计引擎 +- **分布式通信底座(MSYNC)**:服务注册、发现、分布式事务 + +## 2. 技术栈 + +| 层 | 技术 | +|---|---| +| 后端框架 | Django 4.2+ / DRF / SimpleJWT | +| 异步任务 | Celery + Redis(可降级同步) | +| 实时通信 | Socket.IO(python-socketio) | +| 数据库 | MySQL 5.7+(7 库分库路由) | +| 缓存 | Redis(可降级 locmem) | +| 安全 | bcrypt / AES-256-GCM / JWT | +| 前端 | 原生 HTML/JS + 自研 NeoUI/GERPUI 组件 | +| 部署 | Gunicorn + Nginx 反向代理 | + +## 3. 双包架构 + +``` +┌─────────────────────────────────────────────────────┐ +│ d:\SMANAGE(项目根) │ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐│ +│ │ gvsds │ │ gvsdsdk ││ +│ │ 主服务器(业务层) │◄─┤ 分布式 SDK(薄封装)││ +│ │ - Django 项目 │ │ - 协议契约层 ││ +│ │ - 14 个业务 App │ │ - 惰性加载 ││ +│ │ - 完整业务实现 │ │ - PascalCase 协议 ││ +│ └──────────────────────┘ └──────────────────────┘│ +│ ▲ ▲ │ +│ │ │ │ +│ ┌──────────────────────┐ ┌──────────────────────┐│ +│ │ ATestCServer │ │ TempLate/ ││ +│ │ 子服务器示例 │ │ 前端模板资源 ││ +│ │ 通过 SDK 接入 │ │ GERP/AKS/MSOC 等 ││ +│ └──────────────────────┘ └──────────────────────┘│ +└─────────────────────────────────────────────────────┘ +``` + +### 3.1 `gvsds` — 主服务器 + +- 14 个 Django App:`core / gerp / ugm / crb / invite / ccgo / sve / t4 / mc / doc / api / msync / payment / commission / stats` +- 7 库分库路由:`Users / GerpCore / Plans / Tasks / T4PC / Document / Minecraft` +- 6 个自定义中间件:IP 过滤、域名注入、当前角色注入、服务认证、攻击检测、错误日志 + +### 3.2 `gvsdsdk` — 分布式 SDK + +- **薄封装层**:1:1 包装 `gvsds.apps.msync` 原生层,无运行时开销 +- **惰性加载(PEP 562)**:90+ 符号按需 import +- **双类导出**:自有实现(无 Django 依赖,子服务器可用)+ 服务端转发(依赖 Django ORM) +- **三大引擎**:`CommissionEngine`(分佣)、`PaymentManager`(支付)、`TransactionManager`(分布式事务) + +### 3.3 主从通信机制 + +- **版本校验**:启动时 `gvsdsdk.check_min_version('1.0.0')`,主从必须一致 +- **Socket.IO 协议**:20+ 事件,全程 PascalCase 字段 +- **支付网关自动注册**:`gvsdsdk/apps.py` 的 `ready()` 钩子 + +--- + +## 4. SAAS 多租户架构(核心) + +### 4.1 组织四层架构 + +``` +┌──────────────────────────────────────────────────────────┐ +│ 第 4 层:鉴权规则层(运行时) │ +│ 操作权限 (RBAC) 数据权限 (ABAC + RoleScope) │ +├──────────────────────────────────────────────────────────┤ +│ 第 3 层:角色权限载体层 │ +│ Role + UserRole + RolePermission │ +├──────────────────────────────────────────────────────────┤ +│ 第 2 层:岗位模板层(初始化快照) │ +│ PositionTemplate + PositionTemplatePermission │ +│ CopyToRole 一次性复制,后续独立演进 │ +├──────────────────────────────────────────────────────────┤ +│ 第 1 层:组织人事层 │ +│ Tenant → Company → Department │ +│ UserDept(员工-部门多对多) │ +│ Position(人事职务标签,与权限解耦) │ +│ Workgroup(跨部门工作组) Shop(商铺,归公司) │ +└──────────────────────────────────────────────────────────┘ +``` + +### 4.2 数据模型核心表(共 17 张 SAAS 表) + +| 分类 | 表名 | 职责 | +|---|---|---| +| **租户** | `Tenant` | 多租户根,含审核状态/到期时间 | +| **组织** | `Company` / `CompanyInfo` / `Department` / `Workgroup` | 公司树/工商信息/部门树/跨部门组 | +| **用户** | `User` / `UserAvatar` / `User2FA` / `UserPending2FA` / `UserLoginLog` / `AccountActivation` / `Password` / `UserIDCard` | 用户身份与认证 | +| **角色权限** | `Role` / `Permission` / `UserRole` / `RolePermission` | RBAC 核心 | +| **岗位模板** | `PositionTemplate` / `PositionTemplatePermission` | 权限初始化快照 | +| **人事标签** | `Position` / `UserDept` | 职务标签(与权限解耦) | +| **商铺** | `Shop` / `RoleShop` | 多商铺体系 | +| **ABAC** | `UserABAC` | 属性访问控制(补充) | +| **审计** | `AuditLog` / `AbnormalUserLog` | 操作审计 + 攻击溯源 | + +### 4.3 关键设计决策 + +#### (1) Position 与权限**完全解耦** +- 人事职务标签(`Position`)仅用于人事管理,**不直接绑定权限** +- 调岗/升职只修改 `UserDept.PositionUUID` 与 `UserRole`,权限只通过 `UserRole → RolePermission → Permission` 走 +- 规避了业界常见错误:把职务直接绑权限导致调岗时权限树重算 + +#### (2) PositionTemplate 的"快照复制"机制 +- 模板 → 角色 = **一次性复制**(`CopyToRole`),复制后两者无运行时绑定 +- 模板调整只影响**之后新建**的角色,已有角色不被动变更 +- 避免了"模板改一个字段,全员权限突然变了"的运维灾难 + +#### (3) UserRole 作为**胖关联表** +`UserRole` 表同时承载三重语义: +- **权限分配**:User ↔ Role 多对多 +- **组织归属**:携带 `CompanyUUID / DeptUUID / WorkgroupUUID` +- **数据范围**:`RoleType` 标识"普通成员/部门负责人" + +**优势**:同一用户在不同公司/部门可绑不同角色,原生支持 SAAS 强需求 +**代价**:UserRole 违反单一职责,未来扩展需谨慎 + +#### (4) 角色作用域四档数据权限(`RoleScope`) + +| 类型 | 判定条件 | 可见范围 | +|---|---|---| +| `admin` | IsSuperuser/IsStaff 或 `admin:super` 权限 | 全公司 | +| `dept_manager` | `UserRole.RoleType=1` 且 `DeptUUID` 非空 | 本部门成员 | +| `workgroup_lead` | `UserRole.RoleType=1` 且 `WorkgroupUUID` 非空 | 本工作组成员 | +| `member` | 其他 | 仅自己 | + +### 4.4 运行时鉴权流程 + +``` +HTTP Request + │ + ├─► JWTAuthentication → User 实例 + ├─► CurrentRoleMiddleware → request.current_role (Role) + ├─► DomainMiddleware → request.current_company_uuid + │ + ├──► 操作权限校验:PermissionVerifier(permission, attrs, user, request) + │ ├─ ABAC:UserABAC.HierarchicalPermissionIdentifiers ∋ attr? + │ └─ RBAC:RolePermission(角色)→Permission.PermCode ∋ attr? + │ (含请求级缓存,消除 N+1) + │ + └──► 数据权限校验:RoleScope.GetUserScope(request) + └─ ApplyScopeToQueryset(qs, scope, table_name) + 按 admin/dept_manager/workgroup_lead/member 差异化过滤 +``` + +### 4.5 主键设计 + +- 全程使用 `BINARY(16)` UUID 主键 +- 优势:分布式生成、无 ID 冲突、不暴露顺序信息 +- 代价:InnoDB 聚簇索引无序,写入有页分裂开销(可考虑改 UUIDv7) + +--- + +## 5. 三大业务引擎 + +### 5.1 分佣引擎(`CommissionEngine`) + +| 策略 | Type | 说明 | +|---|---|---| +| `PercentageStrategy` | `percentage` | 比例分账,支持保底/封顶 | +| `FixedStrategy` | `fixed` | 固定金额 | +| `TieredStrategy` | `tiered` | 阶梯分账 | +| `RemainderStrategy` | `remainder` | 剩余归平台 | +| `WithdrawalFeeStrategy` | `withdrawal_fee` | 提现手续费 | + +支持:规则配置 → 触发分佣 → 预结算 → 邀请链分佣 → 查询统计 + +### 5.2 支付网关(`PaymentManager`) + +| 渠道 | 支付方式 | 转账 | +|---|---|---| +| 微信支付 | JSAPI/Native/H5/小程序 | 支持 | +| 支付宝 | APP/H5/Native | 支持 | +| 银行卡 | APP/H5/Native | 仅 bank 类型 | + +支持:网关配置 → 支付/查询/退款/转账 → 回调处理 + +### 5.3 统计引擎(`stats`) + +- Celery 定时任务:每日重置 / 每月重置 / 排行榜构建 / 过期清理 +- 按角色差异化仪表盘 + +--- + +## 6. 当前实现状态 + +### 6.1 已完成 + +- ✅ 14 个业务 App 完整实现 +- ✅ 多租户入驻审核工作流 +- ✅ RBAC + ABAC 混合鉴权 +- ✅ 分佣引擎 + 支付网关 + 统计引擎 +- ✅ MSYNC 分布式通信(注册/发现/OGM/Saga 事务) +- ✅ 文件分片上传 + 资产管理 +- ✅ 操作审计日志 + 攻击检测 + +### 6.2 已知技术债 + +| 优先级 | 问题 | 影响 | +|---|---|---| +| 高 | Role/Permission 模型在 gvsds 与 gvsdsdk 双份定义 | 字段漂移风险 | +| 高 | 部分表(User/Dept/Workgroup/Shop)无 TenantUUID | 跨租户查询需 JOIN,删除租户困难 | +| 中 | ABAC 实现本质是 RBAC(字符串集合) | 名实不符,扩展受限 | +| 中 | 数据权限硬编码 `if table_name == 'X'` | 违反开闭原则 | +| 中 | 缺乏软删除机制 | 误删不可恢复 | +| 低 | UUID 主键无序导致索引膨胀 | 大数据量写入性能下降 | +| 低 | 根 README.md 仍为 Flask 时代文档 | 新人易误读 | + +--- + +## 7. 未来发展方向(会议讨论建议项) + +### 7.1 短期(1-3 个月) + +1. **统一 Role/Permission 模型**:让 gerp 直接复用 gvsdsdk 的模型定义,消除双份漂移 +2. **补全 TenantUUID 字段**:所有租户语义表直接打 `TenantUUID`,简化跨租户查询与租户删除 +3. **数据权限策略化**:用策略模式替代 `if table_name == 'X'`,每个模型注册自己的 `ScopeFilter` +4. **更新根 README.md**:反映当前 Django 架构 + +### 7.2 中期(3-6 个月) + +1. **重构 ABAC 为真正的属性访问控制**:支持时间/IP/资源所有者/字段级权限 +2. **引入软删除机制**:`DeletedAt` + `DeletedByUUID`,配合审计日志实现数据恢复 +3. **支付网关扩展**:增加 PayPal/Stripe 适配(出海场景) +4. **租户配额管理**:用户数/数据量/API 调用次数配额 + +### 7.3 长期(6+ 个月) + +1. **拆分协议层**:把 gvsdsdk 拆为 `gvsds-protocol`(纯协议)+ `gvsds-business`(业务能力),主从各自依赖 +2. **改用 UUIDv7 或 Snowflake**:解决主键无序问题 +3. **多区域部署**:基于 MSYNC 实现跨地域多活 +4. **微服务化拆分**:gerp/payment/commission/stats 可独立部署 + +--- + +## 8. 关键架构原则 + +1. **PascalCase 全链路一致**:从数据库字段 → Python 类 → Socket.IO 事件 → 前端,无转换层 +2. **优雅降级**:Redis/Celery/网关依赖缺失时自动降级,开发环境零依赖启动 +3. **配置外置**:`app.ini` + `secrets.ini` + 环境变量三层配置,支持 12-factor 部署 +4. **薄封装 SDK**:协议契约层与业务实现层分离,主从可独立演进 +5. **Position 与权限解耦**:人事标签不绑权限,避免调岗重算 + +--- + +## 9. 联系与维护 + +- **主仓库**:`d:\SMANAGE` +- **SDK 包**:`gvsdsdk/`(pyproject.toml v1.0.0) +- **子服务器示例**:`ATestCServer/` +- **设计文档**:`docs/commission_engine.md` / `docs/payment_gateway.md` / `docs/stats_engine.md` + +--- + +**本架构文档用于技术决策会议参考,详细 API 与字段定义见 [gvsdsdk/API.md](file:///d:/SMANAGE/gvsdsdk/API.md) 与 [gvsdsdk/README.md](file:///d:/SMANAGE/gvsdsdk/README.md)。** diff --git a/TempLate/GERP/_base b/TempLate/GERP/_base index e10ca79a..bb8a7481 100644 --- a/TempLate/GERP/_base +++ b/TempLate/GERP/_base @@ -6,12 +6,9 @@ GERP 管理系统 - - - @@ -22,18 +19,19 @@ - + - + + - + {{ MAIN }} - \ No newline at end of file + diff --git a/TempLate/GERP/assets/css/GERP.css b/TempLate/GERP/assets/css/GERP.css index aac3c224..5c0d2130 100644 --- a/TempLate/GERP/assets/css/GERP.css +++ b/TempLate/GERP/assets/css/GERP.css @@ -1356,7 +1356,6 @@ body.GERPSidebarOpen .GERPSidebar { /* 页面内容样式(用于 iframe 内的页面) */ .GERPPage { padding: 24px; - max-width: 1400px; margin: 0 auto; } diff --git a/TempLate/GERP/assets/gerp/CompanyCommon.js b/TempLate/GERP/assets/gerp/CompanyCommon.js index 5e801b04..515155c4 100644 --- a/TempLate/GERP/assets/gerp/CompanyCommon.js +++ b/TempLate/GERP/assets/gerp/CompanyCommon.js @@ -10,7 +10,7 @@ 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; } + .GERPPage { padding: 24px; 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; } @@ -344,6 +344,12 @@ const CompanyUtils = { ] }); }, + + /** 判断当前 URL 是否显式指定 admin_verify=1 参数(特权模式按需启用) */ + hasAdminVerifyArg() { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('admin_verify') === '1'; + } }; // ==================== TablePage 基类 ==================== @@ -361,6 +367,7 @@ class CompanyTablePage { * @param {Function} [config.onView] - 查看按钮回调 (row) => void,设置后显示眼睛按钮 * @param {string} [config.pkField] - 主键字段名,用于 fetchDetail 按字段过滤 * @param {string} [config.container] - 容器选择器,默认 '#TableContainer' + * @param {boolean} [config.adminBadge] - 是否启用特权模式标记(管理员可关闭公司隔离,跨公司查看/操作数据) */ constructor(config) { this.tableName = config.tableName; @@ -372,24 +379,47 @@ class CompanyTablePage { this.server = CompanyUtils.getServer(); this.tableInstance = null; this.drawerClose = null; + // 特权模式:管理员可关闭公司隔离,跨公司查看/操作数据 + this.adminBadge = config.adminBadge || false; + this.isAdminMode = false; CompanyUtils.setPageTitle(this.title); } - /** 构建带 server 的 FormAPI URL */ + /** 构建带 server 与 admin_verify 的 FormAPI URL */ apiUrl(path, params = {}) { const allParams = {}; if (this.server !== 'gvsds-master') allParams.server = this.server; + if (this.isAdminMode) allParams.admin_verify = '1'; Object.assign(allParams, params); const qs = new URLSearchParams(allParams).toString(); return qs ? `${path}?${qs}` : path; } + /** 检查当前用户是否具有管理员特权(超级用户或当前角色拥有 admin:super 权限) */ + async checkAdmin() { + try { + const resp = await fetch(this.apiUrl('/API/FormAPI/CheckAdmin'), { credentials: 'include' }); + const result = await resp.json(); + return result.code === 0 && result.data && result.data.is_admin; + } catch (e) { + return false; + } + } + /** 初始化表格 */ async init() { const containerEl = document.querySelector(this.container); if (!containerEl) return; + // 特权模式:仅当 URL 显式指定 admin_verify=1 时才检查管理员特权并启用 + // 管理员访问页面默认走普通公司隔离,需手动在 URL 加 ?admin_verify=1 才进入特权模式 + if (this.adminBadge) { + this.isAdminMode = CompanyUtils.hasAdminVerifyArg() ? await this.checkAdmin() : false; + const badge = document.getElementById('admin-badge'); + if (badge) badge.style.display = this.isAdminMode ? '' : 'none'; + } + try { let columns; if (this.server === 'gvsds-master') { @@ -405,8 +435,18 @@ class CompanyTablePage { } // 强公司隔离:CompanyUUID 由后端自动注入,前端隐藏输入框 + // 同时用 defaultColumns 的 type/options 覆盖动态列(后端 SmallIntegerField 生成 number,需前端补充 select 选项) + const defaultMap = {}; + if (this.defaultColumnsFn) { + this.defaultColumnsFn().forEach(c => { defaultMap[c.field] = c; }); + } columns = columns.map(c => { if (c.field === 'CompanyUUID') { c.editable = false; c.hideInForm = true; } + const def = defaultMap[c.field]; + if (def) { + if (def.type) c.type = def.type; + if (def.options) c.options = def.options; + } return c; }); diff --git a/TempLate/GERP/assets/gerp/DbTableHelper.js b/TempLate/GERP/assets/gerp/DbTableHelper.js index 762d5f6a..19668460 100644 --- a/TempLate/GERP/assets/gerp/DbTableHelper.js +++ b/TempLate/GERP/assets/gerp/DbTableHelper.js @@ -25,6 +25,7 @@ class DbTableHelper { serverName, backUrl, backTitle, + adminVerify = false, } = options; if (!tableName) { @@ -34,12 +35,18 @@ class DbTableHelper { } const helper = new DbTableHelper({ - container, tableName, tableTitle, apiMode, subProxyUrl, serverName, backUrl, backTitle, + container, tableName, tableTitle, apiMode, subProxyUrl, serverName, backUrl, backTitle, adminVerify, }); helper._init(); return helper; } + /** 为 URL 追加 admin_verify 参数(管理员豁免,关闭公司隔离) */ + _withAdmin(url) { + if (!this.adminVerify) return url; + return url + (url.includes('?') ? '&' : '?') + 'admin_verify=1'; + } + constructor(options) { Object.assign(this, options); this._tableInstance = null; @@ -52,7 +59,7 @@ class DbTableHelper { try { let columnsResult; if (this.apiMode === 'formapi') { - const resp = await fetch(`/API/FormAPI/Columns/${this.tableName}`, { credentials: 'include' }); + const resp = await fetch(this._withAdmin(`/API/FormAPI/Columns/${this.tableName}`), { credentials: 'include' }); columnsResult = await resp.json(); } else { columnsResult = await this._subPost('columns'); @@ -108,18 +115,18 @@ class DbTableHelper { if (this.apiMode === 'formapi') { tableOpts.api = { - get: `/API/FormAPI/Get/${this.tableName}`, - add: `/API/FormAPI/Add/${this.tableName}`, - edit: `/API/FormAPI/Modify/${this.tableName}/`, - modify: `/API/FormAPI/ModifyItem/${this.tableName}/`, - delete: `/API/FormAPI/Delete/${this.tableName}/`, - columns: `/API/FormAPI/Columns/${this.tableName}`, + get: this._withAdmin(`/API/FormAPI/Get/${this.tableName}`), + add: this._withAdmin(`/API/FormAPI/Add/${this.tableName}`), + edit: this._withAdmin(`/API/FormAPI/Modify/${this.tableName}/`), + modify: this._withAdmin(`/API/FormAPI/ModifyItem/${this.tableName}/`), + delete: this._withAdmin(`/API/FormAPI/Delete/${this.tableName}/`), + columns: this._withAdmin(`/API/FormAPI/Columns/${this.tableName}`), }; tableOpts.onRowClick = () => {}; tableOpts.onDelete = () => {}; } else { tableOpts.api = { - get: `${this.subProxyUrl}?Action=list&Table=${encodeURIComponent(this.tableName)}`, + get: this._withAdmin(`${this.subProxyUrl}?Action=list&Table=${encodeURIComponent(this.tableName)}`), }; tableOpts.onAdd = () => this._handleAdd(); tableOpts.onEdit = (row) => this._handleEdit(row); @@ -132,7 +139,7 @@ class DbTableHelper { // ─── Sub-proxy CRUD ─── async _subPost(action, extraParams) { - const resp = await fetch(this.subProxyUrl, { + const resp = await fetch(this._withAdmin(this.subProxyUrl), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, @@ -162,7 +169,7 @@ class DbTableHelper { if (rowElement) rowElement.classList.add('GERPTableRowLoading'); try { - const getUrl = `${this.subProxyUrl}?Action=list&Table=${encodeURIComponent(this.tableName)}&${pk}=${encodeURIComponent(id)}`; + const getUrl = this._withAdmin(`${this.subProxyUrl}?Action=list&Table=${encodeURIComponent(this.tableName)}&${pk}=${encodeURIComponent(id)}`); const resp = await fetch(getUrl, { credentials: 'include' }); const data = await resp.json(); diff --git a/TempLate/GERP/assets/gerp/gerp.js b/TempLate/GERP/assets/gerp/gerp.js index aef09dd3..1e6d99a1 100644 --- a/TempLate/GERP/assets/gerp/gerp.js +++ b/TempLate/GERP/assets/gerp/gerp.js @@ -15,7 +15,9 @@ class GERP { this.State = { CurrentUser: null, Notifications: [], - Permissions: [] + Permissions: [], + RoleScope: null, // {Type, TypeLabel, IsAdmin, IsDeptManager, IsWorkgroupLead, IsMember, DeptUUID, WorkgroupUUID} + IsAdmin: false }; this.API = { @@ -69,6 +71,24 @@ class GERP { const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const currentHost = window.location.hostname; let _eltExpiredShown = false; + let _refreshing = null; + + function _tryRefreshToken() { + if (_refreshing) return _refreshing; + _refreshing = originalFetch.call(window, 'https://ugm.gvsds.com/Auth/RefreshToken', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' } + }).then(res => res.json()).then(result => { + _refreshing = null; + return result.code === 0; + }).catch(() => { + _refreshing = null; + return false; + }); + return _refreshing; + } + window.fetch = function(url, options = {}) { if (typeof url === 'string') { let isSameOrigin = url.startsWith('/API/') || url.startsWith('/'); @@ -90,6 +110,16 @@ class GERP { } } return originalFetch.call(this, url, options).then(response => { + if (!_eltExpiredShown && response.status === 401) { + return _tryRefreshToken().then(ok => { + if (ok) { + return originalFetch.call(this, url, options); + } + _eltExpiredShown = true; + GERP.ShowELTExpiredDialog(); + return response; + }); + } if (!_eltExpiredShown && response.redirected) { try { const redirectUrl = new URL(response.url); @@ -99,10 +129,6 @@ class GERP { } } catch(e) {} } - if (!_eltExpiredShown && response.status === 401) { - _eltExpiredShown = true; - GERP.ShowELTExpiredDialog(); - } return response; }); }; @@ -677,7 +703,11 @@ class GERP { } // 初始化菜单数据 - InitMenuData() { + async InitMenuData() { + // 先加载角色作用域(用于默认菜单差异化) + if (!this.State.RoleScope) { + await this.LoadRoleScope(); + } fetch('/API/MenuList', { credentials: 'include' }) .then(res => res.json()) .then(result => { @@ -692,39 +722,127 @@ class GERP { }); } + // 加载角色作用域(用于前端差异化显示) + async LoadRoleScope() { + try { + const res = await fetch('/API/FormAPI/CheckAdmin', { credentials: 'include' }); + const result = await res.json(); + if (result.code === 0 && result.data) { + this.State.RoleScope = result.data.role_scope || null; + this.State.IsAdmin = !!(result.data.is_admin || result.data.is_superuser); + } + } catch(e) { + this.State.RoleScope = null; + this.State.IsAdmin = false; + } + } + _getDefaultMenuData() { - return [ + const scope = this.State.RoleScope; + const scopeType = scope?.Type || 'member'; + const isAdmin = scopeType === 'admin'; + const isDeptManager = scopeType === 'dept_manager'; + const isWorkgroupLead = scopeType === 'workgroup_lead'; + const isOrgManager = isAdmin || isDeptManager || isWorkgroupLead; + + // 仪表盘:所有角色都可见 + const menus = [ { Id: 'dashboard', Title: '仪表盘', Icon: '', Page: 'pages/dashboard.html' - }, - { - Id: 'system', - Title: '系统设置', - Icon: '', + } + ]; + + // 公司资产:所有角色都可见(原"系统设置"改名,资产管理挪入此) + // admin 还包含租户/审计/支付/系统配置等系统层面设置 + const companyAssetsChildren = [ + { Id: 'system-assets', Title: '资产管理', Icon: '', Page: 'pages/assets/manager.html' } + ]; + if (isAdmin) { + companyAssetsChildren.push( + { Id: 'system-tenant', Title: '租户管理', Icon: '', Page: 'pages/Company/tenant.html' }, + { Id: 'system-tenant-apply', Title: '租户入驻申请', Icon: '', Page: 'pages/system/tenant-create.html' }, + { Id: 'system-tenant-review', Title: '租户审核工作台', Icon: '', Page: 'pages/system/tenant-review.html' }, + { Id: 'system-audit-log', Title: '操作审计日志', Icon: '', Page: 'pages/system/audit-log.html' }, + { Id: 'system-payment-config', Title: '支付网关配置', Icon: '', Page: 'pages/system/payment-config.html' }, + { Id: 'system-config', Title: '系统配置', Icon: '', Page: 'pages/system/settings.html' } + ); + } else if (isDeptManager || isWorkgroupLead) { + companyAssetsChildren.push( + { Id: 'system-tenant-apply', Title: '租户入驻申请', Icon: '', Page: 'pages/system/tenant-create.html' } + ); + } + menus.push({ + Id: 'company-assets', + Title: '公司资产', + Icon: '', + Expanded: false, + Children: companyAssetsChildren + }); + + // 业务管理:所有角色都可见(销售订单 + 产品管理) + menus.push({ + Id: 'business', + Title: '业务管理', + Icon: '', + Expanded: false, + Children: [ + { Id: 'sales-orders', Title: '销售订单', Icon: '', Page: 'pages/sales/orders.html' }, + { Id: 'inventory-products', Title: '产品管理', Icon: '', Page: 'pages/inventory/products.html' } + ] + }); + + // 组织管理:admin/dept_manager/workgroup_lead 可见 + // 归集公司/部门/工作组/角色/成员/职务等组织相关功能 + if (isOrgManager) { + const orgChildren = []; + if (isAdmin) { + orgChildren.push({ Id: 'org-company', Title: '公司管理', Icon: '', Page: 'pages/Company/company.html' }); + } + orgChildren.push( + { Id: 'org-department', Title: '部门管理', Icon: '', Page: 'pages/Company/department.html' }, + { Id: 'org-workgroup', Title: '工作组管理', Icon: '', Page: 'pages/Company/workgroup.html' }, + { Id: 'org-role', Title: '角色管理', Icon: '', Page: 'pages/Company/role.html' } + ); + if (isAdmin) { + orgChildren.push({ Id: 'org-role-permissions', Title: '角色权限分配', Icon: '', Page: 'pages/system/role-permissions.html' }); + } + // 成员管理(pages/system/users.html 内置 role_scope 差异化:admin=用户管理/dept_manager=部门成员/workgroup_lead=工作组成员) + const memberMenuTitle = isAdmin ? '成员管理' : + (isDeptManager ? '部门成员' : '工作组成员'); + orgChildren.push( + { Id: 'org-employee', Title: memberMenuTitle, Icon: '', Page: 'pages/system/users.html' }, + { Id: 'org-position', Title: '职务管理', Icon: '', Page: 'pages/system/dbtable.html?table=Position&title=职务管理' } + ); + + menus.push({ + Id: 'org', + Title: '组织管理', + Icon: '', Expanded: false, - Children: [ - { Id: 'system-users', Title: '用户管理', Icon: '', Page: 'pages/system/users.html' }, - { Id: 'system-roles', Title: '角色权限', Icon: '', Page: 'pages/system/roles.html' }, - { Id: 'system-positions', Title: '职务管理', Icon: '', Page: 'pages/system/dbtable.html?table=Position&title=职务管理' }, - { Id: 'system-config', Title: '系统配置', Icon: '', Page: 'pages/system/config.html' }, - { Id: 'system-assets', Title: '资产管理', Icon: '', Page: 'pages/assets/manager.html' }, - { Id: 'system-menu', Title: '菜单管理', Icon: '', Page: 'pages/system/menu.html' } - ] - }, - { + Children: orgChildren + }); + } + + // 服务器管理:仅 admin 可见(含菜单编辑器) + if (isAdmin) { + menus.push({ Id: 'server', Title: '服务器管理', Icon: '', Expanded: false, Children: [ { Id: 'server-database', Title: '数据库管理', Icon: '', Page: 'pages/system/database.html' }, - { Id: 'server-msync', Title: 'MSYNC 服务器', Icon: '', Page: 'pages/system/msync-servers.html' } + { Id: 'server-msync', Title: 'MSYNC 服务器', Icon: '', Page: 'pages/system/msync-servers.html' }, + { Id: 'server-menu', Title: '菜单编辑器', Icon: '', Page: 'pages/system/menu.html' } ] - } - ]; + }); + } + // 普通组员:仅看到 仪表盘 + 公司资产(个人资料通过顶部头像菜单访问) + + return menus; } // 打开默认 Tab(仅在主页时) diff --git a/TempLate/GERP/pages/Company/company-detail.html b/TempLate/GERP/pages/Company/company-detail.html index 320c90ec..4706b47c 100644 --- a/TempLate/GERP/pages/Company/company-detail.html +++ b/TempLate/GERP/pages/Company/company-detail.html @@ -1,4 +1,4 @@ - +
@@ -7,6 +7,7 @@

公司详情 +

@@ -19,6 +20,7 @@ +
@@ -6,6 +6,7 @@

公司管理 +

@@ -17,6 +18,7 @@ tableName: 'Company', title: '公司管理', pkField: 'CompanyUUID', + adminBadge: true, defaultColumns: () => [ { field: 'CompanyUUID', title: '公司UUID', width: 140, primaryKey: true, editable: false, hideInForm: true }, { field: 'CompanyName', title: '公司全称', width: 200, type: 'text' }, diff --git a/TempLate/GERP/pages/Company/department-detail.html b/TempLate/GERP/pages/Company/department-detail.html index d3622ba2..555f4ac2 100644 --- a/TempLate/GERP/pages/Company/department-detail.html +++ b/TempLate/GERP/pages/Company/department-detail.html @@ -1,4 +1,4 @@ - +
@@ -120,7 +120,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/UserRole', { DeptUUID: deptUUID }), - add: buildApiUrl('/API/FormAPI/Add/UserRole'), + add: buildApiUrl('/API/FormAPI/Add/UserRole', { DeptUUID: deptUUID }), edit: buildApiUrl('/API/FormAPI/Modify/UserRole/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/'), delete: buildApiUrl('/API/FormAPI/Delete/UserRole/'), diff --git a/TempLate/GERP/pages/Company/employee-detail.html b/TempLate/GERP/pages/Company/employee-detail.html index 22d3a662..7349ed24 100644 --- a/TempLate/GERP/pages/Company/employee-detail.html +++ b/TempLate/GERP/pages/Company/employee-detail.html @@ -1,4 +1,4 @@ - +
@@ -67,18 +67,53 @@ currentData = data; CompanyUtils.setPageTitle(data.UserName || '员工详情'); - // 渲染基本信息 + 表格容器 + // 并行获取 2FA 信息 + let twoFaData = null; + try { + const twoFaResp = await fetch(buildApiUrl('/API/SiteUserManagement/GetUser2FAInfo', { UserUUID: userUUID }), { credentials: 'include' }); + const twoFaResult = await twoFaResp.json(); + if (twoFaResult.code === 0) twoFaData = twoFaResult.data; + } catch (_) { /* 2FA 查询失败不阻塞页面 */ } + + const avatarUrl = buildApiUrl('/Query/GetArgsUserAvatar', { UserUUID: userUUID }); + const avatarFallback = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96'%3E%3Crect fill='%23e5e7eb' width='96' height='96'/%3E%3Ctext x='50%25' y='50%25' font-size='13' fill='%239ca3af' text-anchor='middle' dy='.3em'%3E无头像%3C/text%3E%3C/svg%3E"; + const twoFaStatusHtml = twoFaData + ? (twoFaData.IsEnabled === 1 + ? '已启用' + : '未启用') + : '未配置'; + const twoFaVerifiedHtml = twoFaData + ? (twoFaData.Verified === 1 + ? '已验证' + : '未验证') + : '-'; + const twoFaLastUsed = twoFaData && twoFaData.LastUsedAt ? CompanyUtils.formatTime(twoFaData.LastUsedAt) : '-'; + const twoFaCreateTime = twoFaData && twoFaData.CreateTime ? CompanyUtils.formatTime(twoFaData.CreateTime) : '-'; + + // 渲染基本信息 + 安全信息 + 表格容器 containerEl.innerHTML = `

基本信息

+
+ 头像 +
+
用户名
${CompanyUtils.esc(data.UserName) || '-'}
+
真实姓名
${CompanyUtils.esc(data.UserRealName) || '-'}
+
邮箱
${CompanyUtils.esc(data.UserEmail) || '-'}
+
电话
${CompanyUtils.esc(data.UserTele) || '-'}
+
账号状态
${CompanyUtils.statusTag(data.UserAccountLicense)}
+
是否员工
${data.IsStaff ? '是' : '否'}
+
创建时间
${CompanyUtils.formatTime(data.UserCreateTime)}
+
+
+
+
+

安全信息

-
用户名
${CompanyUtils.esc(data.UserName) || '-'}
-
真实姓名
${CompanyUtils.esc(data.UserRealName) || '-'}
-
邮箱
${CompanyUtils.esc(data.UserEmail) || '-'}
-
电话
${CompanyUtils.esc(data.UserTele) || '-'}
-
账号状态
${CompanyUtils.statusTag(data.UserAccountLicense)}
-
是否员工
${data.IsStaff ? '是' : '否'}
-
创建时间
${CompanyUtils.formatTime(data.UserCreateTime)}
+
2FA状态
${twoFaStatusHtml}
+
验证状态
${twoFaVerifiedHtml}
+
最后使用
${twoFaLastUsed}
+
2FA创建时间
${twoFaCreateTime}
@@ -125,7 +160,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/UserRole', { UserUUID: userUUID }), - add: buildApiUrl('/API/FormAPI/Add/UserRole'), + add: buildApiUrl('/API/FormAPI/Add/UserRole', { UserUUID: userUUID }), edit: buildApiUrl('/API/FormAPI/Modify/UserRole/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/'), delete: buildApiUrl('/API/FormAPI/Delete/UserRole/'), @@ -155,7 +190,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/UserDept', { UserUUID: userUUID }), - add: buildApiUrl('/API/FormAPI/Add/UserDept'), + add: buildApiUrl('/API/FormAPI/Add/UserDept', { UserUUID: userUUID }), edit: buildApiUrl('/API/FormAPI/Modify/UserDept/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/UserDept/'), delete: buildApiUrl('/API/FormAPI/Delete/UserDept/'), diff --git a/TempLate/GERP/pages/Company/role-detail.html b/TempLate/GERP/pages/Company/role-detail.html new file mode 100644 index 00000000..cc420449 --- /dev/null +++ b/TempLate/GERP/pages/Company/role-detail.html @@ -0,0 +1,213 @@ + + +
+
+
+ 返回 +

+ + 角色详情 + +

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

+ + 角色管理 + +

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

+ + 租户详情 + +

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

+ + 租户管理 + +

+
+
+
+
+ + diff --git a/TempLate/GERP/pages/Company/workgroup-detail.html b/TempLate/GERP/pages/Company/workgroup-detail.html index eef482ac..0e4160d3 100644 --- a/TempLate/GERP/pages/Company/workgroup-detail.html +++ b/TempLate/GERP/pages/Company/workgroup-detail.html @@ -1,4 +1,4 @@ - +
@@ -113,7 +113,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/UserRole', { WorkgroupUUID: workgroupUUID }), - add: buildApiUrl('/API/FormAPI/Add/UserRole'), + add: buildApiUrl('/API/FormAPI/Add/UserRole', { WorkgroupUUID: workgroupUUID }), edit: buildApiUrl('/API/FormAPI/Modify/UserRole/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/'), delete: buildApiUrl('/API/FormAPI/Delete/UserRole/'), diff --git a/TempLate/GERP/pages/assets/manager.html b/TempLate/GERP/pages/assets/manager.html index da0dc6af..d9872c80 100644 --- a/TempLate/GERP/pages/assets/manager.html +++ b/TempLate/GERP/pages/assets/manager.html @@ -190,7 +190,7 @@ const pageSize = 24; const CHUNK_SIZE = 5 * 1024 * 1024; const SIMPLE_UPLOAD_THRESHOLD = 5 * 1024 * 1024; - const ASSET_DOWNLOAD_BASE = 'https://api.gvsds.com/Asset/Download'; + const ASSET_DOWNLOAD_BASE = 'https://api.gvsds.com/API/Asset/Download'; const grid = document.getElementById('assetGrid'); const gridWrap = document.getElementById('assetGridWrap'); diff --git a/TempLate/GERP/pages/dashboard.html b/TempLate/GERP/pages/dashboard.html index 8ab4b78c..a697df25 100644 --- a/TempLate/GERP/pages/dashboard.html +++ b/TempLate/GERP/pages/dashboard.html @@ -77,4 +77,51 @@ document.getElementById('btn-personal')?.addEventListener('click', () => { window.GERPApp.HandleUserAction('profile'); } }); + +// ========== 角色差异化统计 ========== +async function loadDashboardStats() { + const gridEl = document.getElementById('statsGrid'); + // 欢迎页无统计卡片容器时静默跳过,避免 null 引用错误 + if (!gridEl) return; + const badgeEl = document.getElementById('scopeBadge'); + gridEl.innerHTML = '
加载中...
'; + try { + const resp = await fetch('/API/Dashboard/Stats', { credentials: 'include' }); + const result = await resp.json(); + if (result.code !== 0) { + gridEl.innerHTML = `
${escapeHtml(result.msg || '加载失败')}
`; + return; + } + const data = result.data || {}; + const stats = data.Stats || {}; + const role = data.Role || {}; + if (badgeEl) badgeEl.textContent = role.TypeLabel || '未知'; + const cards = [ + { icon: 'users', cls: 'users', value: stats.TotalUsers || 0, label: '可见用户' }, + { icon: 'shopping-cart', cls: 'orders', value: stats.TotalOrders || 0, label: '订单总数' }, + { icon: 'calendar-day', cls: 'today', value: stats.TodayOrders || 0, label: '今日订单' }, + { icon: 'clock', cls: 'pending', value: stats.PendingOrders || 0, label: '待确认订单' }, + { icon: 'credit-card', cls: 'paid', value: stats.PaidOrders || 0, label: '已支付订单' }, + { icon: 'check-circle', cls: 'completed', value: stats.CompletedOrders || 0, label: '已完成订单' }, + ]; + gridEl.innerHTML = cards.map(c => ` +
+
+
+
${c.value}
+
${c.label}
+
+
+ `).join(''); + } catch (err) { + gridEl.innerHTML = `
网络错误: ${escapeHtml(err.message)}
`; + } +} + +function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +} + +loadDashboardStats(); \ No newline at end of file diff --git a/TempLate/GERP/pages/doc/editor.html b/TempLate/GERP/pages/doc/editor.html new file mode 100644 index 00000000..6b5f8768 --- /dev/null +++ b/TempLate/GERP/pages/doc/editor.html @@ -0,0 +1,418 @@ + + +
+
+
+

文档编辑器

+
+
+ + + + +
+
+ +
+
+ +
+
加载中...
+
+
+ +
+
+ + +
+
+
请从左侧选择或新建文档
+
+
+
+
+
+ + + diff --git a/TempLate/GERP/pages/inventory/products.html b/TempLate/GERP/pages/inventory/products.html index 233fa825..c9d7134e 100644 --- a/TempLate/GERP/pages/inventory/products.html +++ b/TempLate/GERP/pages/inventory/products.html @@ -1,59 +1,170 @@ -
-
-

产品管理

-

管理产品信息和库存

-
- -
-
-

产品列表

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
产品编号产品名称类别单价库存数量状态操作
PRD-001企业版软件许可软件¥25,000无限在售
PRD-002硬件设备套装 A硬件¥18,500156在售
PRD-003配件套装 B配件¥3,20089库存不足
-
-
- - + + +
+
+
+

+ + 产品管理 + +

+
+ + +
+
+
+ + diff --git a/TempLate/GERP/pages/sales/orders.html b/TempLate/GERP/pages/sales/orders.html index ce1e5d82..2876a96e 100644 --- a/TempLate/GERP/pages/sales/orders.html +++ b/TempLate/GERP/pages/sales/orders.html @@ -1,59 +1,172 @@ -
-
-

销售订单

-

管理和跟踪所有销售订单

-
- -
-
-

订单列表

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
订单号客户名称订单金额下单日期交付日期状态操作
SO-2024-001张三科技有限公司¥25,0002024-03-262024-04-05已交付
SO-2024-002李四制造厂¥18,5002024-03-252024-04-10生产中
SO-2024-003王五贸易公司¥8,0002024-03-252024-04-02待确认
-
-
- - + + +
+
+
+

+ + 销售订单 + +

+
+ + +
+
+
+ + diff --git a/TempLate/GERP/pages/system/audit-log.html b/TempLate/GERP/pages/system/audit-log.html new file mode 100644 index 00000000..29397131 --- /dev/null +++ b/TempLate/GERP/pages/system/audit-log.html @@ -0,0 +1,295 @@ + + +
+
+

+ + 操作审计日志 +

+ +
+
+ + + + + + + + + + +
+
+ + + + + + + + + + + + + +
时间表名操作目标UUIDIP地址操作人详情
+ + + +
+
+ + 第 1 页 + +
+
+ +
+
+

审计日志详情

+
+
+ +
+
+
+ + diff --git a/TempLate/GERP/pages/system/database.html b/TempLate/GERP/pages/system/database.html index 8ffe9981..9f33770e 100644 --- a/TempLate/GERP/pages/system/database.html +++ b/TempLate/GERP/pages/system/database.html @@ -34,6 +34,7 @@

数据库管理 +

@@ -46,6 +47,12 @@

系统表

+ +
@@ -53,17 +60,49 @@ const urlParams = new URLSearchParams(window.location.search); const serverName = urlParams.get('server'); const isSubServer = !!serverName; + let isAdminMode = false; - if (isSubServer) { - document.getElementById('back-nav').style.display = 'flex'; - document.getElementById('section-static').style.display = 'none'; - document.getElementById('section-dynamic').querySelector('h3').textContent = '数据表'; - GERP.SetTitle(`${serverName} — 数据库管理`); - document.getElementById('page-title').textContent = `${serverName} — 数据库管理`; - loadSubTables(); - } else { - GERP.SetTitle('数据库管理'); - loadMainTables(); + /** 检查当前用户是否具有管理员特权 */ + async function checkAdmin() { + try { + const resp = await fetch('/API/FormAPI/CheckAdmin', { credentials: 'include' }); + const result = await resp.json(); + return result.code === 0 && result.data && result.data.is_admin; + } catch (e) { return false; } + } + + function buildApiUrl(path, params = {}) { + const allParams = {}; + if (isSubServer) allParams.server = serverName; + if (isAdminMode) allParams.admin_verify = '1'; + Object.assign(allParams, params); + const qs = new URLSearchParams(allParams).toString(); + return qs ? `${path}?${qs}` : path; + } + + async function init() { + // 入口校验:仅管理员可访问数据库管理页 + const isAdmin = await checkAdmin(); + if (!isAdmin) { + document.getElementById('section-static').style.display = 'none'; + document.getElementById('section-dynamic').style.display = 'none'; + document.getElementById('deniedState').style.display = 'block'; + return; + } + isAdminMode = true; + const badge = document.getElementById('admin-badge'); + if (badge) badge.style.display = 'inline-block'; + if (isSubServer) { + document.getElementById('back-nav').style.display = 'flex'; + document.getElementById('section-static').style.display = 'none'; + document.getElementById('section-dynamic').querySelector('h3').textContent = '数据表'; + GERP.SetTitle(`${serverName} — 数据库管理`); + document.getElementById('page-title').textContent = `${serverName} — 数据库管理`; + loadSubTables(); + } else { + GERP.SetTitle('数据库管理'); + loadMainTables(); + } } function goBack() { @@ -72,7 +111,7 @@ async function loadMainTables() { try { - const response = await fetch('/API/FormAPI/Tables', { credentials: 'include' }); + const response = await fetch(buildApiUrl('/API/FormAPI/Tables'), { credentials: 'include' }); const result = await response.json(); if (result.code === 0) { const tables = result.data || []; @@ -88,7 +127,9 @@ const container = document.getElementById('dynamic-tables'); container.innerHTML = '
加载中...
'; try { - const resp = await fetch(`/api/gerp/msync/servers/${encodeURIComponent(serverName)}/form/?Action=tables`, { credentials: 'include' }); + let subUrl = `/api/gerp/msync/servers/${encodeURIComponent(serverName)}/form/?Action=tables`; + if (isAdminMode) subUrl += '&admin_verify=1'; + const resp = await fetch(subUrl, { credentials: 'include' }); const json = await resp.json(); if (json.code !== 0) { container.innerHTML = `
${json.msg || '加载失败'}
`; @@ -131,4 +172,6 @@ : `?table=${encodeURIComponent(tableName)}&title=${encodeURIComponent(tableTitle)}`; GERP.Navigate(base + params, tableTitle); } + + init(); diff --git a/TempLate/GERP/pages/system/dbtable.html b/TempLate/GERP/pages/system/dbtable.html index 63548843..d7f7d415 100644 --- a/TempLate/GERP/pages/system/dbtable.html +++ b/TempLate/GERP/pages/system/dbtable.html @@ -19,12 +19,18 @@

未选择表 +

+
@@ -36,12 +42,41 @@ const isSubServer = !!serverName; let _helper = null; + let isAdminMode = false; + + /** 检查当前用户是否具有管理员特权 */ + async function checkAdmin() { + try { + const resp = await fetch('/API/FormAPI/CheckAdmin', { credentials: 'include' }); + const result = await resp.json(); + return result.code === 0 && result.data && result.data.is_admin; + } catch (e) { return false; } + } + + function showDenied() { + document.getElementById('TableContainer').style.display = 'none'; + document.getElementById('table-info').style.display = 'none'; + document.getElementById('deniedState').style.display = 'block'; + } + + async function initPage() { + if (!tableName) { + document.getElementById('table-title').textContent = '未选择表'; + document.getElementById('TableContainer').innerHTML = + '
未指定表名
'; + return; + } + + // 入口校验:仅管理员可访问数据库表浏览页 + const isAdmin = await checkAdmin(); + if (!isAdmin) { + showDenied(); + return; + } + isAdminMode = true; + const badge = document.getElementById('admin-badge'); + if (badge) badge.style.display = 'inline-block'; - if (!tableName) { - document.getElementById('table-title').textContent = '未选择表'; - document.getElementById('TableContainer').innerHTML = - '
未指定表名
'; - } else { if (isSubServer) { GERP.SetTitle(`${serverName} / ${tableTitle}`); document.getElementById('table-title').textContent = `${serverName} — ${tableTitle}`; @@ -55,6 +90,7 @@ serverName: serverName, backUrl: `/pages/system/database.html?server=${encodeURIComponent(serverName)}`, backTitle: `${serverName} 数据库管理`, + adminVerify: isAdminMode, }); } else { GERP.SetTitle(tableTitle); @@ -66,7 +102,10 @@ apiMode: 'formapi', backUrl: '/pages/system/database.html', backTitle: '数据库管理', + adminVerify: isAdminMode, }); } } + + initPage(); diff --git a/TempLate/GERP/pages/system/menu.html b/TempLate/GERP/pages/system/menu.html index 6eb71339..28231e9a 100644 --- a/TempLate/GERP/pages/system/menu.html +++ b/TempLate/GERP/pages/system/menu.html @@ -96,6 +96,7 @@

菜单编辑器 +

@@ -191,6 +192,11 @@
+ @@ -211,6 +217,53 @@ let clipboard = null; let animatingUUID = null; let ctxTargetUUID = null; + let isAdminMode = false; + let _accessVerified = false; // 入口校验是否已通过 + const currentServer = new URLSearchParams(window.location.search).get('server') || 'gvsds-master'; + + /** 检查当前用户是否具有管理员特权 */ + async function checkAdmin() { + try { + const resp = await fetch(buildApiUrl('/API/FormAPI/CheckAdmin'), { credentials: 'include' }); + const result = await resp.json(); + return result.code === 0 && result.data && result.data.is_admin; + } catch (e) { return false; } + } + + function buildApiUrl(path, params = {}) { + const allParams = {}; + if (currentServer !== 'gvsds-master') allParams.server = currentServer; + if (isAdminMode) allParams.admin_verify = '1'; + Object.assign(allParams, params); + const qs = new URLSearchParams(allParams).toString(); + return qs ? `${path}?${qs}` : path; + } + + // 入口校验:仅管理员可访问菜单编辑器;非管理员显示无权访问 + function showDenied() { + const editor = document.querySelector('.menu-editor'); + if (editor) { + // 隐藏编辑器内的所有交互元素,但保留 deniedState + Array.from(editor.children).forEach(el => { + if (el.id !== 'deniedState') el.style.display = 'none'; + }); + } + const denied = document.getElementById('deniedState'); + if (denied) denied.style.display = 'block'; + } + + // 同步先隐藏关键交互,待异步校验通过后再启用 + const _pendingVerify = checkAdmin().then(r => { + if (!r) { + showDenied(); + return false; + } + _accessVerified = true; + isAdminMode = true; + const badge = document.getElementById('admin-badge'); + if (badge) badge.style.display = 'inline-block'; + return true; + }); const menuTabs = document.getElementById('menuTabs'); const tabAddBtn = document.getElementById('tabAddBtn'); @@ -358,7 +411,7 @@ const kw = tabAddSearch.value.trim(); if (!kw) { tabAddResults.innerHTML = ''; return; } tabSearchTimer = setTimeout(() => { - fetch(`/API/FormAPI/CompanySearch?keyword=${encodeURIComponent(kw)}&limit=10`, { credentials: 'include' }) + fetch(buildApiUrl('/API/FormAPI/CompanySearch', { keyword: kw, limit: 10 }), { credentials: 'include' }) .then(r => r.json()) .then(res => { if (res.code === 0 && res.data && res.data.length) { @@ -389,7 +442,7 @@ companySearchClear.style.display = kw ? 'block' : 'none'; if (!kw) { companySearchDropdown.classList.remove('open'); return; } companySearchTimer = setTimeout(() => { - fetch(`/API/FormAPI/CompanySearch?keyword=${encodeURIComponent(kw)}&limit=10`, { credentials: 'include' }) + fetch(buildApiUrl('/API/FormAPI/CompanySearch', { keyword: kw, limit: 10 }), { credentials: 'include' }) .then(r => r.json()) .then(res => { if (res.code === 0 && res.data && res.data.length) { @@ -435,12 +488,15 @@ async function loadMenus() { const tab = getActiveTab(); if (!tab) return; + // 入口校验:未通过管理员校验则不加载 + if (!_accessVerified) { await _pendingVerify; } + if (!_accessVerified) return; try { - const resp = await fetch('/API/FormAPI/Get/Menu?PageSize=200', { credentials: 'include' }); + // 使用专门的菜单管理 API,后端按公司隔离返回(公有 + 当前公司私有) + const resp = await fetch(buildApiUrl('/API/Menu/AdminList', { CompanyUUID: tab.companyUUID }), { credentials: 'include' }); const result = await resp.json(); if (result.code === 0) { - const allMenus = result.data || []; - tab.menus = allMenus.filter(m => !m.CompanyUUID || m.CompanyUUID === tab.companyUUID); + tab.menus = result.data || []; } else { tab.menus = []; } } catch(e) { tab.menus = []; } renderPreview(); @@ -697,7 +753,7 @@ }; let actualUUID = null; try { - const resp = await fetch('/API/FormAPI/Add/Menu', { + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(data).toString() @@ -780,7 +836,7 @@ }; let actualUUID = null; try { - const resp = await fetch('/API/FormAPI/Add/Menu', { + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(data).toString() @@ -843,12 +899,26 @@ }; } - async function saveMenuSort(u, newSort) { + async function saveMenuSort(menuObj, newSort) { try { - const resp = await fetch(`/API/FormAPI/ModifyItem/Menu/${u}`, { - method: 'PATCH', credentials: 'include', + const data = { + MenuUUID: menuObj.MenuUUID, + CompanyUUID: menuObj.CompanyUUID || '', + ParentMenuUUID: menuObj.ParentMenuUUID || '', + MenuName: menuObj.MenuName || '', + MenuTitle: menuObj.MenuTitle || '', + MenuIcon: menuObj.MenuIcon || '', + MenuLink: menuObj.MenuLink || '', + MenuType: menuObj.MenuType != null ? menuObj.MenuType : 0, + MenuSort: newSort, + MenuStatus: menuObj.MenuStatus != null ? menuObj.MenuStatus : 1, + PermissionLevel: menuObj.PermissionLevel != null ? menuObj.PermissionLevel : 1.0, + PermissionAttr: menuObj.PermissionAttr || '', + }; + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { + method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ MenuSort: newSort }).toString() + body: new URLSearchParams(data).toString() }); const result = await resp.json(); return result.code === 0; @@ -867,8 +937,8 @@ const targetSort = target.MenuSort || 0; current.MenuSort = targetSort; target.MenuSort = currentSort; - const ok1 = await saveMenuSort(current.MenuUUID, current.MenuSort); - const ok2 = await saveMenuSort(target.MenuUUID, target.MenuSort); + const ok1 = await saveMenuSort(current, current.MenuSort); + const ok2 = await saveMenuSort(target, target.MenuSort); if (ok1 && ok2) { renderPreview(); } else { current.MenuSort = currentSort; target.MenuSort = targetSort; await loadMenus(); } } @@ -877,9 +947,10 @@ const tab = getActiveTab(); if (!tab || !tab.selectedUUID) return; const data = getFormData(); + data.MenuUUID = tab.selectedUUID; try { - const resp = await fetch(`/API/FormAPI/ModifyItem/Menu/${tab.selectedUUID}`, { - method: 'PATCH', credentials: 'include', + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { + method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(data).toString() }); @@ -904,7 +975,7 @@ MenuType: 1, MenuSort: maxSort + 1, PermissionLevel: 1.0, PermissionAttr: '', MenuStatus: 1, }; try { - const resp = await fetch('/API/FormAPI/Add/Menu', { + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(data).toString() @@ -924,7 +995,11 @@ const nodeEl = menuPreview.querySelector(`[data-uuid="${u}"]`); if (nodeEl) { nodeEl.classList.add('anim-out'); await new Promise(r => setTimeout(r, 280)); } try { - const resp = await fetch(`/API/FormAPI/Delete/Menu/${u}`, { method: 'DELETE', credentials: 'include' }); + const resp = await fetch(buildApiUrl('/API/Menu/Delete'), { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ MenuUUID: u }).toString() + }); const result = await resp.json(); if (result.code === 0) { if (tab.selectedUUID === u) { tab.selectedUUID = null; updateFormState(); } @@ -946,7 +1021,7 @@ MenuLink: '', MenuType: 0, MenuSort: maxSort + 1, PermissionLevel: 1.0, PermissionAttr: '', MenuStatus: 1, }; try { - const resp = await fetch('/API/FormAPI/Add/Menu', { + const resp = await fetch(buildApiUrl('/API/Menu/Save'), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(data).toString() diff --git a/TempLate/GERP/pages/system/msync-servers.html b/TempLate/GERP/pages/system/msync-servers.html index c1f7dabd..e755b648 100644 --- a/TempLate/GERP/pages/system/msync-servers.html +++ b/TempLate/GERP/pages/system/msync-servers.html @@ -145,6 +145,7 @@

MSYNC 服务器管理 +

@@ -165,6 +166,24 @@ GERP.SetTitle('MSYNC 服务器管理'); const API_BASE = '/api/gerp/msync/servers/'; + let isAdminMode = false; + + /** 检查当前用户是否具有管理员特权 */ + async function checkAdmin() { + try { + const resp = await fetch('/API/FormAPI/CheckAdmin', { credentials: 'include' }); + const result = await resp.json(); + return result.code === 0 && result.data && result.data.is_admin; + } catch (e) { return false; } + } + + function buildApiUrl(path, params = {}) { + const allParams = {}; + if (isAdminMode) allParams.admin_verify = '1'; + Object.assign(allParams, params); + const qs = new URLSearchParams(allParams).toString(); + return qs ? `${path}?${qs}` : path; + } // 时区工具 const userTZ = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -213,8 +232,18 @@ const statsEl = document.getElementById('stats'); listEl.innerHTML = '

加载中...

'; + // 检查管理员特权(每次刷新时检查,仅当 URL 带 admin_verify=1 时启用) + if (!isAdminMode) { + const _hasAdminVerify = new URLSearchParams(window.location.search).get('admin_verify') === '1'; + isAdminMode = _hasAdminVerify ? await checkAdmin() : false; + if (isAdminMode) { + const badge = document.getElementById('admin-badge'); + if (badge) badge.style.display = 'inline-block'; + } + } + try { - const resp = await fetch(API_BASE, { credentials: 'include' }); + const resp = await fetch(buildApiUrl(API_BASE), { credentials: 'include' }); const json = await resp.json(); if (json.code === 403) { @@ -348,7 +377,7 @@ async function viewServerDetail(name) { try { - const resp = await fetch(`${API_BASE}${encodeURIComponent(name)}/`, { credentials: 'include' }); + const resp = await fetch(buildApiUrl(`${API_BASE}${encodeURIComponent(name)}/`), { credentials: 'include' }); const json = await resp.json(); if (json.code !== 0) { if (window.GERPUI) window.GERPUI.ShowToast({ message: json.msg || '获取详情失败', type: 'error' }); @@ -398,7 +427,7 @@ type: 'danger', onConfirm: async () => { try { - const resp = await fetch(`${API_BASE}${encodeURIComponent(name)}/deregister/`, { + const resp = await fetch(buildApiUrl(`${API_BASE}${encodeURIComponent(name)}/deregister/`), { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, diff --git a/TempLate/GERP/pages/system/payment-config.html b/TempLate/GERP/pages/system/payment-config.html new file mode 100644 index 00000000..60ad6ecd --- /dev/null +++ b/TempLate/GERP/pages/system/payment-config.html @@ -0,0 +1,344 @@ + + +
+
+

+ + 支付网关配置 +

+ + +
+
+ + + +
+
+ +
+
+

新增支付网关配置

+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + diff --git a/TempLate/GERP/pages/system/role-detail.html b/TempLate/GERP/pages/system/role-detail.html index d6769e38..ef527e6d 100644 --- a/TempLate/GERP/pages/system/role-detail.html +++ b/TempLate/GERP/pages/system/role-detail.html @@ -1,4 +1,4 @@ - +
@@ -10,6 +10,7 @@ +
加载中...
@@ -34,7 +35,6 @@ let currentData = null; let userRoleTable = null; let permTable = null; - let adminTable = null; let isAdminMode = false; /** 检查当前用户是否具有管理员特权 */ @@ -78,8 +78,8 @@ return; } - // 先检查管理员特权 - isAdminMode = await checkAdmin(); + // 先检查管理员特权(仅当 URL 带 admin_verify=1 时启用) + isAdminMode = CompanyUtils.hasAdminVerifyArg() ? await checkAdmin() : false; const adminBadge = document.getElementById('admin-badge'); if (isAdminMode) { adminBadge.style.display = 'inline-block'; @@ -131,7 +131,6 @@ initUserRoleTable(); initPermTable(); - initAdminTable(); } catch (e) { containerEl.innerHTML = `
网络错误: ${CompanyUtils.esc(e.message)}
`; } @@ -164,7 +163,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/UserRole', { RoleUUID: roleUUID }), - add: buildApiUrl('/API/FormAPI/Add/UserRole'), + add: buildApiUrl('/API/FormAPI/Add/UserRole', { RoleUUID: roleUUID }), edit: buildApiUrl('/API/FormAPI/Modify/UserRole/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/UserRole/'), delete: buildApiUrl('/API/FormAPI/Delete/UserRole/'), @@ -201,7 +200,7 @@ bordered: true, api: { get: buildApiUrl('/API/FormAPI/Get/RolePermission', { RoleUUID: roleUUID }), - add: buildApiUrl('/API/FormAPI/Add/RolePermission'), + add: buildApiUrl('/API/FormAPI/Add/RolePermission', { RoleUUID: roleUUID }), edit: buildApiUrl('/API/FormAPI/Modify/RolePermission/'), modify: buildApiUrl('/API/FormAPI/ModifyItem/RolePermission/'), delete: buildApiUrl('/API/FormAPI/Delete/RolePermission/'), @@ -212,40 +211,6 @@ }); } - function initAdminTable() { - adminTable = new TableEngine({ - container: '#admin-table-container', - height: '200px', - tableName: 'RoleAdmin', - columns: [ - { field: 'RoleAdminUUID', title: '主键UUID', primaryKey: true, editable: false, hideInForm: true }, - { field: 'RoleUUID', title: '角色UUID', editable: false, hideInForm: true }, - { field: 'CreateTime', title: '创建时间', type: 'datetime', editable: false }, - ], - data: [], - pageSize: 20, - columnDistribution: 'equal', - showPagination: true, - showCheckbox: true, - editable: true, - sortable: true, - filterable: true, - striped: true, - hoverable: true, - bordered: true, - api: { - get: buildApiUrl('/API/FormAPI/Get/RoleAdmin', { RoleUUID: roleUUID }), - add: buildApiUrl('/API/FormAPI/Add/RoleAdmin'), - edit: buildApiUrl('/API/FormAPI/Modify/RoleAdmin/'), - modify: buildApiUrl('/API/FormAPI/ModifyItem/RoleAdmin/'), - delete: buildApiUrl('/API/FormAPI/Delete/RoleAdmin/'), - }, - onView: (row) => { - // 无需跳转 - }, - }); - } - /** 编辑按钮:弹出编辑对话框 */ async function handleEdit() { if (!currentData) return; @@ -257,5 +222,17 @@ }); } + /** 跳转到权限树分配页面 */ + function goPermissionTree() { + if (!roleUUID) { + if (window.GERPLayui) GERPLayui.Toast('缺少 RoleUUID', 'warning'); + return; + } + let url = `/pages/system/role-permissions.html?RoleUUID=${encodeURIComponent(roleUUID)}`; + if (currentServer !== 'gvsds-master') url += `&server=${encodeURIComponent(currentServer)}`; + if (isAdminMode) url += '&admin_verify=1'; + GERP.Navigate(url, '角色权限分配'); + } + init(); diff --git a/TempLate/GERP/pages/system/role-permissions.html b/TempLate/GERP/pages/system/role-permissions.html new file mode 100644 index 00000000..a4fccb22 --- /dev/null +++ b/TempLate/GERP/pages/system/role-permissions.html @@ -0,0 +1,406 @@ + + + + +
+
+
+ + 返回 + +

+ + 角色权限分配 +

+ +
+ +
+ + + + +
共 0 项权限,已勾选 0 项
+
+ + + + +
+
+ +
+
+ +
请先选择角色和公司
+
+ + +
+
+ + + + +
+
+ + diff --git a/TempLate/GERP/pages/system/roles.html b/TempLate/GERP/pages/system/roles.html index 3f4b5753..b40b0c53 100644 --- a/TempLate/GERP/pages/system/roles.html +++ b/TempLate/GERP/pages/system/roles.html @@ -50,8 +50,9 @@ async function initTable() { const containerEl = document.getElementById('TableContainer'); - // 先检查管理员特权 - _isAdminMode = await checkAdmin(); + // 先检查管理员特权(仅当 URL 带 admin_verify=1 时启用) + const _hasAdminVerify = new URLSearchParams(window.location.search).get('admin_verify') === '1'; + _isAdminMode = _hasAdminVerify ? await checkAdmin() : false; try { let columns; diff --git a/TempLate/GERP/pages/system/tenant-create.html b/TempLate/GERP/pages/system/tenant-create.html index b501059c..948415a7 100644 --- a/TempLate/GERP/pages/system/tenant-create.html +++ b/TempLate/GERP/pages/system/tenant-create.html @@ -17,9 +17,9 @@
- 创建租户 + 租户入驻申请
-
创建新的租户主体。租户创建后,企业可申请入驻该租户下。
+
提交租户入驻申请,等待超管或审核员审核通过后激活。
@@ -32,19 +32,23 @@
请选择有效期
+
+ + +
- +